6 * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
7 * @author Cornelius Weiss <c.weiss@metaways.de>
8 * @copyright Copyright (c) 2010-2017 Metaways Infosystems GmbH (http://www.metaways.de)
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 or has no attendee");
168 $periods = $this->getBlockingPeriods($_event, array(
169 'from' => $_event->dtstart,
170 'until' => $_event->dtstart->getClone()->addMonth(2)
173 $fbInfo = $this->getFreeBusyInfo($periods, $_event->attendee, $ignoreUIDs);
175 if (count($fbInfo) > 0) {
176 $busyException = new Calendar_Exception_AttendeeBusy();
177 $busyException->setFreeBusyInfo($fbInfo);
179 Calendar_Model_Attender::resolveAttendee($_event->attendee, /* resolve_display_containers = */ false);
180 $busyException->setEvent($_event);
182 throw $busyException;
185 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
186 . " Free/busy check: no conflict found");
192 * @param Tinebase_Record_Interface $_record
193 * @param bool $_checkBusyConflicts
194 * @return Tinebase_Record_Interface
195 * @throws Tinebase_Exception_AccessDenied
196 * @throws Tinebase_Exception_Record_Validation
198 public function create(Tinebase_Record_Interface $_record, $_checkBusyConflicts = FALSE, $skipEvent = false)
201 $db = $this->_backend->getAdapter();
202 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
204 $this->_inspectEvent($_record, $skipEvent);
206 // we need to resolve groupmembers before free/busy checking
207 Calendar_Model_Attender::resolveGroupMembers($_record->attendee);
209 if ($_checkBusyConflicts) {
210 // ensure that all attendee are free
211 $this->checkBusyConflicts($_record);
214 $sendNotifications = $this->_sendNotifications;
215 $this->_sendNotifications = FALSE;
217 $createdEvent = parent::create($_record);
219 $this->_sendNotifications = $sendNotifications;
221 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
222 } catch (Exception $e) {
223 Tinebase_TransactionManager::getInstance()->rollBack();
227 // send notifications
228 if ($this->_sendNotifications && $_record->mute != 1) {
229 $this->doSendNotifications($createdEvent, Tinebase_Core::getUser(), 'created');
232 return $createdEvent;
236 * inspect creation of one record (after create)
238 * @param Tinebase_Record_Interface $_createdRecord
239 * @param Tinebase_Record_Interface $_record
242 protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interface $_record)
244 $this->_saveAttendee($_record, $_createdRecord);
248 * deletes a recur series
250 * @param Calendar_Model_Event $_recurInstance
253 public function deleteRecurSeries($_recurInstance)
255 $baseEvent = $this->getRecurBaseEvent($_recurInstance);
256 $this->delete($baseEvent->getId());
262 * @param string $_orderBy Order result by
263 * @param string $_orderDirection Order direction - allowed are ASC and DESC
264 * @throws Tinebase_Exception_InvalidArgument
265 * @return Tinebase_Record_RecordSet
267 public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
269 throw new Tinebase_Exception_NotImplemented('not implemented');
273 * returns period filter expressing blocking times caused by given event
275 * @param Calendar_Model_Event $event
276 * @param array $checkPeriod array(
277 "from" => DateTime (defaults to dtstart)
278 "until" => DateTime (defaults to dtstart + 2 years)
279 "max" => Integer (defaults to 25)
281 * @return Calendar_Model_EventFilter
283 public function getBlockingPeriods($event, $checkPeriod = array())
285 $eventSet = new Tinebase_Record_RecordSet('Calendar_Model_Event', array($event));
287 if (! empty($event->rrule)) {
289 $eventSet->merge($this->getRecurExceptions($event, true));
290 } catch (Tinebase_Exception_NotFound $e) {
291 // it's ok, event is not exising yet so we don't have exceptions as well
294 $from = isset($checkPeriod['from']) ? $checkPeriod['from'] : clone $event->dtstart;
295 $until = isset($checkPeriod['until']) ? $checkPeriod['until'] : $from->getClone()->addMonth(24);
296 Calendar_Model_Rrule::mergeRecurrenceSet($eventSet, $from, $until);
299 $periodFilters = array();
300 foreach ($eventSet as $candidate) {
301 if ($candidate->transp != Calendar_Model_Event::TRANSP_TRANSP) {
302 $periodFilters[] = array(
304 'operator' => 'within',
306 'from' => $candidate->dtstart,
307 'until' => $candidate->dtend,
313 $filter = new Calendar_Model_EventFilter($periodFilters, Tinebase_Model_Filter_FilterGroup::CONDITION_OR);
319 * returns conflicting periods
321 * @param Calendar_Model_EventFilter $periodCandidates
322 * @param Calendar_Model_EventFilter $conflictCriteria
323 * @param bool $getAll
326 public function getConflictingPeriods($periodCandidates, $conflictCriteria, $getAll=false)
328 $conflictFilter = clone $conflictCriteria;
329 $conflictFilter->addFilterGroup($periodCandidates);
330 $conflictCandidates = $this->search($conflictFilter);
332 $from = $until = false;
333 foreach ($periodCandidates as $periodFilter) {
334 $period = $periodFilter->getValue();
335 $from = $from ? min($from, $period['from']) : $period['from'];
336 $until = $until ? max($until, $period['until']) : $period['until'];
338 Calendar_Model_Rrule::mergeRecurrenceSet($conflictCandidates, $from, $until);
340 $conflicts = array();
341 foreach ($periodCandidates as $periodFilter) {
342 $period = $periodFilter->getValue();
344 foreach($conflictCandidates as $event) {
345 if ($event->dtstart->isEarlier($period['until']) && $event->dtend->isLater($period['from'])) {
346 $conflicts[] = array(
347 'from' => $period['from'],
348 'until' => $period['until'],
363 * @param Tinebase_Record_RecordSet $_records
366 public function mergeFreeBusyInfo(Tinebase_Record_RecordSet $_records)
368 $_records->sort('dtstart');
370 /** @var Calendar_Model_Event $event */
371 foreach($_records as $event) {
372 foreach($result as $key => &$period) {
373 if ($event->dtstart->isEarlierOrEquals($period['dtend'])) {
374 if ($event->dtend->isLaterOrEquals($period['dtstart'])) {
375 if ($event->dtstart->isEarlier($period['dtstart'])) {
376 $period['dtstart'] = clone $event->dtstart;
378 if ($event->dtend->isLater($period['dtend'])) {
379 $period['dtend'] = clone $event->dtend;
383 throw new Tinebase_Exception_UnexpectedValue('record set sort by dtstart did not work!');
388 'dtstart' => $event->dtstart,
389 'dtend' => $event->dtend
397 * returns freebusy information for given period and given attendee
399 * @todo merge overlapping events to one freebusy entry
401 * @param Calendar_Model_EventFilter $_periods
402 * @param Tinebase_Record_RecordSet $_attendee
403 * @param array $_ignoreUIDs
404 * @return Tinebase_Record_RecordSet of Calendar_Model_FreeBusy
406 public function getFreeBusyInfo($_periods, $_attendee, $_ignoreUIDs = array())
408 $fbInfoSet = new Tinebase_Record_RecordSet('Calendar_Model_FreeBusy');
410 // map groupmembers to users
411 $attendee = clone $_attendee;
412 $groupmembers = $attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
413 $groupmembers->user_type = Calendar_Model_Attender::USERTYPE_USER;
414 /*$groups = $attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUP);
415 $attendee->removeRecords($groups);
416 /** @var Calendar_Model_Attender $group *
417 foreach($groups as $group) {
418 $group = Tinebase_Group::getInstance()->getGroupById($group->user_id);
420 // fetch list only if list_id is not NULL, otherwise we get back an empty list object
421 if (!empty($group->list_id)) {
422 $contactList = Addressbook_Controller_List::getInstance()->get($group->list_id);
423 foreach ($contactList->members as $member) {
424 $attendee->addRecord(new Calendar_Model_Attender(array(
425 'user_id' => $member,
426 'user_type' => Calendar_Model_Attender::USERTYPE_USER
432 $conflictCriteria = new Calendar_Model_EventFilter(array(
433 array('field' => 'attender', 'operator' => 'in', 'value' => $attendee),
434 array('field' => 'transp', 'operator' => 'equals', 'value' => Calendar_Model_Event::TRANSP_OPAQUE)
437 $conflictingPeriods = $this->getConflictingPeriods($_periods, $conflictCriteria, true);
441 foreach ($attendee as $attender) {
442 if (! isset($typeMap[$attender['user_type']])) {
443 $typeMap[$attender['user_type']] = array();
445 if (is_object($attender['user_id'])) {
446 $attender['user_id'] = $attender['user_id']->getId();
448 $typeMap[$attender['user_type']][$attender['user_id']] = array();
450 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' ' . __LINE__
451 . ' value: ' . print_r($typeMap, true));
453 // fetch resources to get freebusy type
454 // NOTE: we could add busy_type to attendee later
455 if (isset($typeMap[Calendar_Model_Attender::USERTYPE_RESOURCE])) {
456 $resources = Calendar_Controller_Resource::getInstance()->getMultiple(array_keys($typeMap[Calendar_Model_Attender::USERTYPE_RESOURCE]), true);
459 $processedEvents = array();
461 foreach ($conflictingPeriods as $conflictingPeriod) {
462 $event = $conflictingPeriod['event'];
464 // one event may conflict multiple periods
465 if (in_array($event, $processedEvents)) {
469 $processedEvents[] = $event;
471 // skip events with ignoreUID
472 if (in_array($event->uid, $_ignoreUIDs)) {
476 // map groupmembers to users
477 $groupmembers = $event->attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
478 $groupmembers->user_type = Calendar_Model_Attender::USERTYPE_USER;
480 foreach ($event->attendee as $attender) {
481 // skip declined/transp events
482 if ($attender->status == Calendar_Model_Attender::STATUS_DECLINED ||
483 $attender->transp == Calendar_Model_Event::TRANSP_TRANSP) {
487 if ((isset($typeMap[$attender->user_type]) || array_key_exists($attender->user_type, $typeMap)) && (isset($typeMap[$attender->user_type][$attender->user_id]) || array_key_exists($attender->user_id, $typeMap[$attender->user_type]))) {
488 $type = Calendar_Model_FreeBusy::FREEBUSY_BUSY;
490 if ($attender->user_type == Calendar_Model_Attender::USERTYPE_RESOURCE) {
491 $resource = $resources->getById($attender->user_id);
493 $type = $resource->busy_type;
497 $fbInfo = new Calendar_Model_FreeBusy(array(
498 'user_type' => $attender->user_type,
499 'user_id' => $attender->user_id,
500 'dtstart' => clone $event->dtstart,
501 'dtend' => clone $event->dtend,
505 if ($event->{Tinebase_Model_Grants::GRANT_READ}) {
506 $fbInfo->event = clone $event;
507 unset($fbInfo->event->attendee);
510 //$typeMap[$attender->user_type][$attender->user_id][] = $fbInfo;
511 $fbInfoSet->addRecord($fbInfo);
520 * update future constraint exdates of a single event
524 public function setConstraintsExdates($_record)
526 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
527 . ' event has rrule constrains, calculating exdates');
529 $exdates = is_array($_record->exdate) ? $_record->exdate : array();
531 // own event should not trigger constraints conflicts
532 if ($_record->rrule_constraints && $_record->rrule_constraints instanceof Calendar_Model_EventFilter) {
533 $constraints = clone $_record->rrule_constraints;
534 $constraints->addFilter(new Tinebase_Model_Filter_Text('uid', 'not', $_record->uid));
536 $constrainExdatePeriods = $this->getConflictingPeriods($this->getBlockingPeriods($_record), $constraints);
537 foreach ($constrainExdatePeriods as $constrainExdatePeriod) {
538 $exdates[] = $constrainExdatePeriod['from'];
542 $_record->exdate = array_unique($exdates);
546 * update all future constraints exdates
548 public function updateConstraintsExdates()
550 // find all future recur events with constraints (ignoring ACL's)
551 $constraintsEventIds = $this->_backend->search(new Calendar_Model_EventFilter(array(
552 array('field' => 'period', 'operator' => 'within', 'value' => array(
553 'from' => Tinebase_DateTime::now(),
554 'until' => Tinebase_DateTime::now()->addMonth(24)
556 array('field' => 'rrule_contraints', 'operator' => 'notnull', 'value' => NULL)
560 foreach ($constraintsEventIds as $constraintsEventId) {
562 $event = $this->_backend->get($constraintsEventId);
563 $this->setConstraintsExdates($event);
564 // NOTE: touch also updates
565 $this->_touch($event);
566 } catch (Exception $e) {
567 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
568 . " cannot update constraints exdates for event {$constraintsEventId}: " . $e->getMessage());
569 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
570 . " cannot update constraints exdates for event {$constraintsEventId}: " . $e);
576 * get list of records
578 * @param Tinebase_Model_Filter_FilterGroup $_filter
579 * @param Tinebase_Model_Pagination $_pagination
580 * @param bool $_getRelations
581 * @param boolean $_onlyIds
582 * @param string $_action for right/acl check
583 * @return Tinebase_Record_RecordSet|array
585 public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
587 $events = parent::search($_filter, $_pagination, $_getRelations, $_onlyIds, $_action);
589 $this->_freeBusyCleanup($events, $_action);
596 * Returns a set of records identified by their id's
598 * @param array $_ids array of record identifiers
599 * @param bool $_ignoreACL don't check acl grants
600 * @return Tinebase_Record_RecordSet of $this->_modelName
602 public function getMultiple($_ids, $_ignoreACL = false)
604 $events = parent::getMultiple($_ids, $_ignoreACL = false);
605 if ($_ignoreACL !== true) {
606 $this->_freeBusyCleanup($events, 'get');
613 * cleanup search results (freebusy)
615 * @param Tinebase_Record_RecordSet $_events
616 * @param string $_action
618 protected function _freeBusyCleanup(Tinebase_Record_RecordSet $_events, $_action)
620 foreach ($_events as $event) {
621 $doFreeBusyCleanup = $event->doFreeBusyCleanup();
622 if ($doFreeBusyCleanup && $_action !== 'get') {
623 $_events->removeRecord($event);
629 * @param Calendar_Model_Event $_event with
630 * attendee to find free timeslot for
631 * dtstart, dtend -> to calculate duration
633 * @param array $_options
634 * 'from' datetime (optional, defaults event->dtstart) from where to start searching
635 * 'until' datetime (optional, defaults 2 years) until when to giveup searching
636 * 'constraints' array (optional, defaults to 8-20 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR') array of timespecs to limit the search with
640 * rrule ... for example "work days" -> 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR'
641 * @return Tinebase_Record_RecordSet record set of event sugestions
642 * @throws Tinebase_Exception_NotImplemented
644 public function searchFreeTime($_event, $_options)
646 $functionTime = time();
648 // validate $_event, originator_tz will be validated by setTimezone() call
649 if (!isset($_event->dtstart) || !$_event->dtstart instanceof Tinebase_DateTime) {
650 throw new Tinebase_Exception_UnexpectedValue('dtstart needs to be set');
652 if (!isset($_event->dtstart) || !$_event->dtstart instanceof Tinebase_DateTime) {
653 throw new Tinebase_Exception_UnexpectedValue('dtend needs to be set');
655 if (!isset($_event->attendee) || !$_event->attendee instanceof Tinebase_Record_RecordSet ||
656 $_event->attendee->count() < 1) {
657 throw new Tinebase_Exception_UnexpectedValue('attendee needs to be set and contain at least one attendee');
660 if (empty($_event->originator_tz)) {
661 $_event->originator_tz = Tinebase_Core::getUserTimezone();
664 $from = isset($_options['from']) ? ($_options['from'] instanceof Tinebase_DateTime ? $_options['from'] :
665 new Tinebase_DateTime($_options['from'])) : clone $_event->dtstart;
666 $until = isset($_options['until']) ? ($_options['until'] instanceof Tinebase_DateTime ? $_options['until'] :
667 new Tinebase_DateTime($_options['until'])) : $_event->dtend->getClone()->addYear(2);
669 $currentFrom = $from->getClone();
670 $currentUntil = $from->getClone()->addDay(6)->setTime(23, 59, 59);
671 if ($currentUntil->isLater($until)) {
672 $currentUntil = clone $until;
674 $durationSec = (int)$_event->dtend->getTimestamp() - (int)$_event->dtstart->getTimestamp();
675 $constraints = new Tinebase_Record_RecordSet('Calendar_Model_Event', array());
676 $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event', array());
678 if (isset($_options['constraints'])) {
679 foreach ($_options['constraints'] as $constraint) {
680 if (!isset($constraint['dtstart']) || !isset($constraint['dtend'])) {
684 $constraint['uid'] = Tinebase_Record_Abstract::generateUID();
685 $event = new Calendar_Model_Event(array(), true);
686 $event->setFromJsonInUsersTimezone($constraint);
687 $event->originator_tz = $_event->originator_tz;
688 $constraints->addRecord($event);
692 if ($constraints->count() === 0) {
693 //here the timezone will come from the getClone, not need to set it
694 $constraints->addRecord(new Calendar_Model_Event(
696 'uid' => Tinebase_Record_Abstract::generateUID(),
697 'dtstart' => $currentFrom->getClone()->setHour(8)->setMinute(0)->setSecond(0),
698 'dtend' => $currentFrom->getClone()->setHour(20)->setMinute(0)->setSecond(0),
699 'rrule' => 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR',
700 'originator_tz' => $_event->originator_tz
706 if (time() - $functionTime > 23) {
707 $exception = new Calendar_Exception_AttendeeBusy();
708 $exception->setEvent(new Calendar_Model_Event(array('dtend' => $currentFrom), true));
712 $currentConstraints = clone $constraints;
713 Calendar_Model_Rrule::mergeRecurrenceSet($currentConstraints, $currentFrom, $currentUntil);
714 $currentConstraints->sort('dtstart');
717 // sort out constraints that do not fit the rrule
718 if (!empty($_event->rrule)) {
719 /** @var Calendar_Model_Event $event */
720 foreach ($currentConstraints as $event) {
721 $recurEvent = clone $_event;
722 $recurEvent->uid = Tinebase_Record_Abstract::generateUID();
723 $recurEvent->dtstart = $event->dtstart->getClone()->subDay(1);
724 if ($_event->is_all_day_event) {
725 $recurEvent->dtend = $event->dtend->getClone()->subDay(1);
727 $recurEvent->dtend = $recurEvent->dtstart->getClone()->addSecond($durationSec);
729 if (null === ($recurEvent = Calendar_Model_Rrule::computeNextOccurrence($recurEvent, $exceptions, $event->dtstart))
730 || $recurEvent->dtstart->isLater($event->dtend)) {
734 foreach($remove as $event) {
735 $currentConstraints->removeRecord($event);
739 if ($currentConstraints->count() > 0) {
741 /** @var Calendar_Model_Event $event */
742 foreach ($currentConstraints as $event) {
745 'operator' => 'within',
747 'from' => $event->dtstart,
748 'until' => $event->dtend
753 $busySlots = $this->getFreeBusyInfo(new Calendar_Model_EventFilter($periods, Tinebase_Model_Filter_FilterGroup::CONDITION_OR), $_event->attendee);
754 $busySlots = $this->mergeFreeBusyInfo($busySlots);
756 /** @var Calendar_Model_Event $event */
757 foreach ($currentConstraints as $event) {
758 if ($event->dtend->isEarlierOrEquals($currentFrom)) {
762 if ($_event->is_all_day_event) {
763 $durationSec = (int)$event->dtend->getTimestamp() - (int)$event->dtstart->getTimestamp();
766 $constraintStart = (int)$event->dtstart->getTimestamp();
767 if ($constraintStart < (int)$currentFrom->getTimestamp()) {
768 $constraintStart = (int)$currentFrom->getTimestamp();
770 $constraintEnd = (int)$event->dtend->getTimestamp();
771 if ($constraintEnd > (int)$currentUntil->getTimestamp()) {
772 $constraintEnd = (int)$currentUntil->getTimestamp();
774 $lastBusyEnd = $constraintStart;
776 /** @var Calendar_Model_FreeBusy $busy */
777 foreach ($busySlots as $key => $busy) {
778 $busyStart = (int)$busy['dtstart']->getTimestamp();
779 $busyEnd = (int)$busy['dtend']->getTimestamp();
781 if ($busyEnd < $constraintStart) {
786 if ($busyStart > ($constraintEnd - $durationSec)) {
787 $lastBusyEnd = $busyEnd;
791 if (($lastBusyEnd + $durationSec) <= $busyStart) {
792 // check between $lastBusyEnd and $busyStart
793 $result = $this->_tryForFreeSlot($_event, $lastBusyEnd, $busyStart, $durationSec, $until);
794 if ($result->count() > 0) {
795 if ($_event->is_all_day_event) {
796 $result->getFirstRecord()->dtstart = $_event->dtstart;
797 $result->getFirstRecord()->dtend = $_event->dtend;
802 $lastBusyEnd = $busyEnd;
804 foreach ($remove as $key) {
805 unset($busySlots[$key]);
808 if (($lastBusyEnd + $durationSec) <= $constraintEnd) {
809 // check between $lastBusyEnd and $constraintEnd
810 $result = $this->_tryForFreeSlot($_event, $lastBusyEnd, $constraintEnd, $durationSec, $until);
811 if ($result->count() > 0) {
812 if ($_event->is_all_day_event) {
813 $result->getFirstRecord()->dtstart = $_event->dtstart;
814 $result->getFirstRecord()->dtend = $_event->dtend;
822 $currentFrom->addDay(7)->setTime(0, 0, 0);
823 $currentUntil->addDay(7);
824 if ($currentUntil->isLater($until)) {
825 $currentUntil = clone $until;
827 } while ($until->isLater($currentFrom));
829 return new Tinebase_Record_RecordSet('Calendar_Model_Event', array());
832 protected function _tryForFreeSlot(Calendar_Model_Event $_event, $_startSec, $_endSec, $_durationSec, Tinebase_DateTime $_until)
834 $event = new Calendar_Model_Event(array(
835 'uid' => Tinebase_Record_Abstract::generateUID(),
836 'dtstart' => new Tinebase_DateTime($_startSec),
837 'dtend' => new Tinebase_DateTime($_startSec + $_durationSec),
838 'originator_tz' => $_event->originator_tz,
840 $result = new Tinebase_Record_RecordSet('Calendar_Model_Event', array($event));
842 if (!empty($_event->rrule)) {
843 $event->rrule = $_event->rrule;
845 $until = $event->dtstart->getClone()->addMonth(2);
846 if ($until->isLater($_until)) {
849 $periods = $this->getBlockingPeriods($event, array(
850 'from' => $event->dtstart,
853 $busySlots = $this->getFreeBusyInfo($periods, $_event->attendee);
854 $event->dtstart->addMinute(15);
855 $event->dtend->addMinute(15);
856 } while($busySlots->count() > 0 && $event->dtend->getTimestamp() <= $_endSec && $event->dtend->isEarlierOrEquals($_until));
858 if ($busySlots->count() > 0) {
859 $result->removeAll();
861 $event->dtstart->subMinute(15);
862 $event->dtend->subMinute(15);
872 * @param Tinebase_Record_Interface $_record
873 * @param bool $_checkBusyConflicts
874 * @param string $range
875 * @return Tinebase_Record_Interface
876 * @throws Tinebase_Exception_AccessDenied
877 * @throws Tinebase_Exception_Record_Validation
879 public function update(Tinebase_Record_Interface $_record, $_checkBusyConflicts = FALSE, $range = Calendar_Model_Event::RANGE_THIS, $skipEvent = false)
881 /** @var Calendar_Model_Event $_record */
883 $db = $this->_backend->getAdapter();
884 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
886 $sendNotifications = $this->sendNotifications(FALSE);
888 $event = $this->get($_record->getId());
889 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
890 .' Going to update the following event. rawdata: ' . print_r($event->toArray(), true));
892 //NOTE we check via get(full rights) here whereas _updateACLCheck later checks limited rights from search
893 if ($this->_doContainerACLChecks === FALSE || $event->hasGrant(Tinebase_Model_Grants::GRANT_EDIT)) {
894 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " updating event: {$_record->id} (range: {$range})");
896 // we need to resolve groupmembers before free/busy checking
897 Calendar_Model_Attender::resolveGroupMembers($_record->attendee);
898 $this->_inspectEvent($_record, $skipEvent);
900 if ($_checkBusyConflicts) {
901 if ($event->isRescheduled($_record) ||
902 count(array_diff($_record->attendee->user_id, $event->attendee->user_id)) > 0
904 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
905 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
906 . " Ensure that all attendee are free with free/busy check ... ");
908 $this->checkBusyConflicts($_record);
910 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
911 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
912 . " Skipping free/busy check because event has not been rescheduled and no new attender has been added");
917 parent::update($_record);
919 } else if ($_record->attendee instanceof Tinebase_Record_RecordSet) {
920 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
921 . " user has no editGrant for event: {$_record->id}, updating attendee status with valid authKey only");
922 foreach ($_record->attendee as $attender) {
923 if ($attender->status_authkey) {
924 $eventUpdateEvent = new Calendar_Event_EventUpdateEvent();
925 $eventUpdateEvent->observable = $_record;
926 Tinebase_Record_PersistentObserver::getInstance()->fireEvent($eventUpdateEvent);
928 $this->attenderStatusUpdate($_record, $attender, $attender->status_authkey);
933 if ($_record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
934 $this->_updateExdateRange($_record, $range, $event);
937 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
938 } catch (Exception $e) {
939 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Rolling back because: ' . $e);
940 Tinebase_TransactionManager::getInstance()->rollBack();
941 $this->sendNotifications($sendNotifications);
945 $updatedEvent = $this->get($event->getId());
947 // send notifications
948 $this->sendNotifications($sendNotifications);
949 if ($this->_sendNotifications && $_record->mute != 1) {
950 $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
952 return $updatedEvent;
956 * inspect update of one record (after update)
958 * @param Tinebase_Record_Interface $updatedRecord the just updated record
959 * @param Tinebase_Record_Interface $record the update record
960 * @param Tinebase_Record_Interface $currentRecord the current record (before update)
963 protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
965 $this->_saveAttendee($record, $currentRecord, $record->isRescheduled($currentRecord));
966 // need to save new attendee set in $updatedRecord for modlog
967 $updatedRecord->attendee = clone($record->attendee);
971 * update range of events starting with given recur exception
973 * @param Calendar_Model_Event $exdate
974 * @param string $range
976 protected function _updateExdateRange($exdate, $range, $oldExdate)
978 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
979 . ' Updating events (range: ' . $range . ') belonging to recur exception event ' . $exdate->getId());
981 $baseEvent = $this->getRecurBaseEvent($exdate);
982 /** @var Tinebase_Record_Diff $diff */
983 $diff = $oldExdate->diff($exdate);
985 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
986 . ' Exdate diff: ' . print_r($diff->toArray(), TRUE));
988 if ($range === Calendar_Model_Event::RANGE_ALL) {
989 $events = $this->getRecurExceptions($baseEvent);
990 $events->addRecord($baseEvent);
991 $this->_applyExdateDiffToRecordSet($exdate, $diff, $events);
992 } else if ($range === Calendar_Model_Event::RANGE_THISANDFUTURE) {
993 $nextRegularRecurEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $exdate->dtstart);
995 if ($nextRegularRecurEvent == $baseEvent) {
996 // NOTE if a fist instance exception takes place before the
997 // series would start normally, $nextOccurence is the
998 // baseEvent of the series. As createRecurException can't
999 // deal with this situation we update whole series here
1000 $this->_updateExdateRange($exdate, Calendar_Model_Event::RANGE_ALL, $oldExdate);
1001 } else if ($nextRegularRecurEvent !== NULL && ! $nextRegularRecurEvent->dtstart->isEarlier($exdate->dtstart)) {
1002 $this->_applyDiff($nextRegularRecurEvent, $diff, $exdate, FALSE);
1004 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1005 . ' Next recur exception event at: ' . $nextRegularRecurEvent->dtstart->toString());
1006 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1007 . ' ' . print_r($nextRegularRecurEvent->toArray(), TRUE));
1009 $nextRegularRecurEvent->mute = $exdate->mute;
1010 $newBaseEvent = $this->createRecurException($nextRegularRecurEvent, FALSE, TRUE);
1011 // @todo this should be done by createRecurException
1012 $exdatesOfNewBaseEvent = $this->getRecurExceptions($newBaseEvent);
1013 $this->_applyExdateDiffToRecordSet($exdate, $diff, $exdatesOfNewBaseEvent);
1015 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1016 . ' No upcoming occurrences found.');
1021 protected function _applyDTStartToBaseEventRRULE($_dtstart, $_baseEvent)
1023 /** @var Calendar_Model_Rrule $rrule */
1024 /*$rrule = $_baseEvent->rrule;
1025 if (! $rrule instanceof Calendar_Model_Rrule) {
1026 $rrule = new Calendar_Model_Rrule($rrule);
1029 switch($rrule->freq)
1031 case Calendar_Model_Rrule::FREQ_DAILY
1034 if ($rrule->freq == Calendar_Model_Rrule::FREQ_WEEKLY) {
1038 if ($rrule->freq == Calendar_Model_Rrule::FREQ_MONTHLY) {
1039 if (!empty($rrule->byday)) {
1041 } elseif(!empty($rrule->bymonthday)) {
1046 if ($rrule->freq == Calendar_Model_Rrule::FREQ_YEARLY) {
1047 // bymonthday + bymonth
1050 switch($rrule->byday)
1057 * apply exdate diff to a recordset of events
1059 * @param Calendar_Model_Event $exdate
1060 * @param Tinebase_Record_Diff $diff
1061 * @param Tinebase_Record_RecordSet $events
1063 protected function _applyExdateDiffToRecordSet($exdate, $diff, $events)
1065 // make sure baseEvent gets updated first to circumvent concurrency conflicts
1066 $events->sort('recurdid', 'ASC');
1068 foreach ($events as $event) {
1069 if ($event->getId() === $exdate->getId()) {
1073 $this->_applyDiff($event, $diff, $exdate, FALSE);
1074 $this->update($event);
1079 * merge updates from exdate into event
1081 * @param Calendar_Model_Event $event
1082 * @param Tinebase_Record_Diff $diff
1083 * @param Calendar_Model_Event $exdate
1084 * @param boolean $overwriteMods
1086 * @todo is $overwriteMods needed?
1088 protected function _applyDiff($event, $diff, $exdate, $overwriteMods = TRUE)
1090 if (! $overwriteMods) {
1091 $recentChanges = Tinebase_Timemachine_ModificationLog::getInstance()->getModifications('Calendar', $event, NULL, 'Sql', $exdate->creation_time)->filter('change_type', Tinebase_Timemachine_ModificationLog::UPDATED);
1092 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1093 . ' Recent changes (since ' . $exdate->creation_time->toString() . '): ' . print_r($recentChanges->toArray(), TRUE));
1095 $recentChanges = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog');
1098 $changedAttributes = Tinebase_Timemachine_ModificationLog::getModifiedAttributes($recentChanges);
1099 $diffIgnore = array('organizer', 'seq', 'last_modified_by', 'last_modified_time', 'dtstart', 'dtend');
1100 foreach ($diff->diff as $key => $newValue) {
1101 if ($key === 'attendee') {
1102 if (in_array($key, $changedAttributes)) {
1103 $attendeeDiff = $diff->diff['attendee'];
1104 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1105 . ' Attendee diff: ' . print_r($attendeeDiff->toArray(), TRUE));
1106 foreach ($attendeeDiff['added'] as $attenderToAdd) {
1107 $attenderToAdd->setId(NULL);
1108 $event->attendee->addRecord($attenderToAdd);
1110 foreach ($attendeeDiff['removed'] as $attenderToRemove) {
1111 $attenderInCurrentSet = Calendar_Model_Attender::getAttendee($event->attendee, $attenderToRemove);
1112 if ($attenderInCurrentSet) {
1113 $event->attendee->removeRecord($attenderInCurrentSet);
1117 // remove ids of new attendee
1118 $attendee = clone($exdate->attendee);
1119 foreach ($attendee as $attender) {
1120 if (! $event->attendee->getById($attender->getId())) {
1121 $attender->setId(NULL);
1124 $event->attendee = $attendee;
1126 } else if (! in_array($key, $diffIgnore) && ! in_array($key, $changedAttributes)) {
1127 $event->{$key} = $exdate->{$key};
1129 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1130 . ' Ignore / recently changed: ' . $key);
1134 if ((isset($diff->diff['dtstart']) || array_key_exists('dtstart', $diff->diff)) || (isset($diff->diff['dtend']) || array_key_exists('dtend', $diff->diff))) {
1135 $this->_applyTimeDiff($event, $exdate);
1140 * update multiple records
1142 * @param Tinebase_Model_Filter_FilterGroup $_filter
1143 * @param array $_data
1144 * @return integer number of updated records
1146 public function updateMultiple($_filter, $_data)
1148 $this->_checkRight('update');
1149 $this->checkFilterACL($_filter, 'update');
1152 $ids = $this->_backend->search($_filter, NULL, TRUE);
1154 foreach ($ids as $eventId) {
1155 $event = $this->get($eventId);
1156 foreach ($_data as $field => $value) {
1157 $event->$field = $value;
1160 $this->update($event);
1167 * Deletes a set of records.
1169 * If one of the records could not be deleted, no record is deleted
1171 * @param array $_ids array of record identifiers
1172 * @param string $range
1174 * @throws Tinebase_Exception_NotFound|Tinebase_Exception
1176 public function delete($_ids, $range = Calendar_Model_Event::RANGE_THIS)
1178 if ($_ids instanceof $this->_modelName) {
1179 $_ids = (array)$_ids->getId();
1182 $records = $this->_backend->getMultiple((array) $_ids);
1184 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1185 . " Deleting " . count($records) . ' with range ' . $range . ' ...');
1187 foreach ($records as $record) {
1188 if ($record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
1189 $this->_deleteExdateRange($record, $range);
1193 $db = $this->_backend->getAdapter();
1194 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1196 // delete if delete grant is present
1197 if ($this->_doContainerACLChecks === FALSE || $record->hasGrant(Tinebase_Model_Grants::GRANT_DELETE)) {
1198 // NOTE delete needs to update sequence otherwise iTIP based protocolls ignore the delete
1199 $record->status = Calendar_Model_Event::STATUS_CANCELED;
1200 $this->_touch($record);
1201 if ($record->isRecurException()) {
1203 $baseEvent = $this->getRecurBaseEvent($record);
1204 $this->_touch($baseEvent);
1205 } catch (Tinebase_Exception_NotFound $tnfe) {
1206 // base Event might be gone already
1207 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1208 . " BaseEvent of exdate {$record->uid} to delete not found ");
1212 parent::delete($record);
1215 // otherwise update status for user to DECLINED
1216 else if ($record->attendee instanceof Tinebase_Record_RecordSet) {
1217 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " user has no deleteGrant for event: " . $record->id . ", updating own status to DECLINED only");
1218 $ownContact = Tinebase_Core::getUser()->contact_id;
1219 foreach ($record->attendee as $attender) {
1220 if ($attender->user_id == $ownContact && in_array($attender->user_type, array(Calendar_Model_Attender::USERTYPE_USER, Calendar_Model_Attender::USERTYPE_GROUPMEMBER))) {
1221 $attender->status = Calendar_Model_Attender::STATUS_DECLINED;
1222 $this->attenderStatusUpdate($record, $attender, $attender->status_authkey);
1227 // increase display container content sequence for all attendee of deleted event
1228 if ($record->attendee instanceof Tinebase_Record_RecordSet) {
1229 foreach ($record->attendee as $attender) {
1230 $this->_increaseDisplayContainerContentSequence($attender, $record, Tinebase_Model_ContainerContent::ACTION_DELETE);
1234 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1235 } catch (Exception $e) {
1236 Tinebase_TransactionManager::getInstance()->rollBack();
1243 * delete range of events starting with given recur exception
1245 * NOTE: if exdate is persistent, it will not be deleted by this function
1246 * but by the original call of delete
1248 * @param Calendar_Model_Event $exdate
1249 * @param string $range
1251 protected function _deleteExdateRange($exdate, $range)
1253 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1254 . ' Deleting events (range: ' . $range . ') belonging to recur exception event ' . $exdate->getId());
1256 $baseEvent = $this->getRecurBaseEvent($exdate);
1258 if ($range === Calendar_Model_Event::RANGE_ALL) {
1259 $this->deleteRecurSeries($exdate);
1260 } else if ($range === Calendar_Model_Event::RANGE_THISANDFUTURE) {
1261 $nextRegularRecurEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $exdate->dtstart);
1263 if ($nextRegularRecurEvent == $baseEvent) {
1264 // NOTE if a fist instance exception takes place before the
1265 // series would start normally, $nextOccurence is the
1266 // baseEvent of the series. As createRecurException can't
1267 // deal with this situation we delete whole series here
1268 $this->_deleteExdateRange($exdate, Calendar_Model_Event::RANGE_ALL);
1270 $this->createRecurException($nextRegularRecurEvent, TRUE, TRUE);
1276 * updates a recur series
1278 * @param Calendar_Model_Event $_recurInstance
1279 * @param bool $_checkBusyConflicts
1280 * @return Calendar_Model_Event
1282 public function updateRecurSeries($_recurInstance, $_checkBusyConflicts = FALSE)
1284 $baseEvent = $this->getRecurBaseEvent($_recurInstance);
1286 $originalDtStart = $_recurInstance->getOriginalDtStart();
1287 $originalDtStart->setTimezone(Tinebase_DateTime::TIMEZONE_UTC);
1288 $dtStart = $_recurInstance->dtstart;
1289 if (! $dtStart instanceof Tinebase_DateTime) {
1290 $dtStart = new Tinebase_DateTime($dtStart);
1292 $dtStart->setTimezone(Tinebase_DateTime::TIMEZONE_UTC);
1293 $dtEnd = $_recurInstance->dtEnd;
1294 if (! $dtEnd instanceof Tinebase_DateTime) {
1295 $dtEnd = new Tinebase_DateTime($dtEnd);
1297 $dtEnd->setTimezone(Tinebase_DateTime::TIMEZONE_UTC);
1299 if ($originalDtStart->compare($dtStart) !== 0 ||
1300 (($orgDiff = $baseEvent->dtend->diff($baseEvent->dtstart)) &&
1301 ($newDiff = $dtEnd->diff($dtStart)) &&
1303 $orgDiff->days !== $newDiff->days ||
1304 $orgDiff->h !== $newDiff->h ||
1305 $orgDiff->m !== $newDiff->m ||
1306 $orgDiff->s !== $newDiff->s
1309 if (strpos($baseEvent->rrule->byday, ',') !== false ||
1310 strpos($baseEvent->rrule->bymonthday, ',') !== false ) {
1311 throw new Tinebase_Exception_UnexpectedValue('dont change complex stuff like that');
1315 // replace baseEvent with adopted instance
1316 $newBaseEvent = clone $_recurInstance;
1317 $newBaseEvent->setId($baseEvent->getId());
1318 unset($newBaseEvent->recurid);
1319 $newBaseEvent->exdate = $baseEvent->exdate;
1321 $this->_applyTimeDiff($newBaseEvent, $_recurInstance, $baseEvent);
1323 return $this->update($newBaseEvent, $_checkBusyConflicts);
1329 * @param Calendar_Model_Event $newEvent
1330 * @param Calendar_Model_Event $fromEvent
1331 * @param Calendar_Model_Event $baseEvent
1333 protected function _applyTimeDiff($newEvent, $fromEvent, $baseEvent = NULL)
1336 $baseEvent = $newEvent;
1339 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1340 . ' New event: ' . print_r($newEvent->toArray(), TRUE));
1341 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1342 . ' From event: ' . print_r($fromEvent->toArray(), TRUE));
1344 // compute time diff (NOTE: if the $fromEvent is the baseEvent, it has no recurid)
1345 $originalDtStart = $fromEvent->recurid ? new Tinebase_DateTime(substr($fromEvent->recurid, -19), 'UTC') : clone $baseEvent->dtstart;
1347 $dtstartDiff = $originalDtStart->diff($fromEvent->dtstart);
1348 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1349 . " Dtstart diff: " . $dtstartDiff->format('%H:%M:%i'));
1350 $eventDuration = $fromEvent->dtstart->diff($fromEvent->dtend);
1351 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1352 . " Duration diff: " . $dtstartDiff->format('%H:%M:%i'));
1354 $newEvent->dtstart = clone $baseEvent->dtstart;
1355 $newEvent->dtstart->add($dtstartDiff);
1357 $newEvent->dtend = clone $newEvent->dtstart;
1358 $newEvent->dtend->add($eventDuration);
1362 * creates an exception instance of a recurring event
1364 * NOTE: deleting persistent exceptions is done via a normal delete action
1365 * and handled in the deleteInspection
1367 * @param Calendar_Model_Event $_event
1368 * @param bool $_deleteInstance
1369 * @param bool $_allFollowing
1370 * @param bool $_checkBusyConflicts
1371 * @return Calendar_Model_Event exception Event | updated baseEvent
1373 * @todo replace $_allFollowing param with $range
1374 * @deprecated replace with create/update/delete
1376 public function createRecurException($_event, $_deleteInstance = FALSE, $_allFollowing = FALSE, $_checkBusyConflicts = FALSE)
1378 $baseEvent = $this->getRecurBaseEvent($_event);
1380 if ($baseEvent->last_modified_time != $_event->last_modified_time) {
1381 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1382 . " It is not allowed to create recur instance if it is clone of base event");
1383 throw new Tinebase_Timemachine_Exception_ConcurrencyConflict('concurrency conflict!');
1387 // // exdates needs to stay in baseEvents container
1388 // if ($_event->container_id != $baseEvent->container_id) {
1389 // throw new Calendar_Exception_ExdateContainer();
1392 // check if this is an exception to the first occurence
1393 if ($baseEvent->getId() == $_event->getId()) {
1394 if ($_allFollowing) {
1395 throw new Exception('please edit or delete complete series!');
1397 // NOTE: if the baseEvent gets a time change, we can't compute the recurdid w.o. knowing the original dtstart
1398 $recurid = $baseEvent->setRecurId($baseEvent->getId());
1399 unset($baseEvent->recurid);
1400 $_event->recurid = $recurid;
1403 // just do attender status update if user has no edit grant
1404 if ($this->_doContainerACLChecks && !$baseEvent->{Tinebase_Model_Grants::GRANT_EDIT}) {
1405 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1406 . " user has no editGrant for event: '{$baseEvent->getId()}'. Only creating exception for attendee status");
1407 if ($_event->attendee instanceof Tinebase_Record_RecordSet) {
1408 foreach ($_event->attendee as $attender) {
1409 if ($attender->status_authkey) {
1410 $exceptionAttender = $this->attenderStatusCreateRecurException($_event, $attender, $attender->status_authkey, $_allFollowing);
1415 if (! isset($exceptionAttender)) {
1416 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG) && $_event->attendee instanceof Tinebase_Record_RecordSet) {
1417 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Failed to update attendee: " . print_r($_event->attendee->toArray(), true));
1419 throw new Tinebase_Exception_AccessDenied('Failed to update attendee, status authkey might be missing');
1422 return $this->get($exceptionAttender->cal_event_id);
1425 // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
1426 if (empty($_event->recurid)) {
1427 throw new Exception('recurid must be present to create exceptions!');
1430 // we do notifications ourself
1431 $sendNotifications = $this->sendNotifications(FALSE);
1433 // EDIT for baseEvent is checked above, CREATE, DELETE for recur exceptions is implied with it
1434 $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
1436 $db = $this->_backend->getAdapter();
1437 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1439 $exdate = new Tinebase_DateTime(substr($_event->recurid, -19));
1440 $exdates = is_array($baseEvent->exdate) ? $baseEvent->exdate : array();
1441 $originalDtstart = $_event->getOriginalDtStart();
1442 $originalEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $originalDtstart);
1444 if ($_allFollowing != TRUE) {
1445 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1446 . " Adding exdate for: '{$_event->recurid}'");
1448 array_push($exdates, $exdate);
1449 $baseEvent->exdate = $exdates;
1450 $updatedBaseEvent = $this->update($baseEvent, FALSE);
1452 if ($_deleteInstance == FALSE) {
1453 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1454 . " Creating persistent exception for: '{$_event->recurid}'");
1455 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1456 . " Recur exception: " . print_r($_event->toArray(), TRUE));
1458 $_event->base_event_id = $baseEvent->getId();
1459 $_event->setId(NULL);
1460 unset($_event->rrule);
1461 unset($_event->exdate);
1463 foreach (array('attendee', 'notes', 'alarms') as $prop) {
1464 if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
1465 $_event->{$prop}->setId(NULL);
1469 $originalDtstart = $_event->getOriginalDtStart();
1470 $dtStartHasDiff = $originalDtstart->compare($_event->dtstart) != 0; // php52 compat
1472 if (! $dtStartHasDiff) {
1473 $attendees = $_event->attendee;
1474 unset($_event->attendee);
1476 $note = $_event->notes;
1477 unset($_event->notes);
1478 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts);
1480 if (! $dtStartHasDiff) {
1481 // we save attendee seperatly to preserve their attributes
1482 if ($attendees instanceof Tinebase_Record_RecordSet) {
1483 $attendees->cal_event_id = $persistentExceptionEvent->getId();
1484 $calendar = Tinebase_Container::getInstance()->getContainerById($_event->container_id);
1485 foreach ($attendees as $attendee) {
1486 $this->_createAttender($attendee, $_event, TRUE, $calendar);
1487 $this->_increaseDisplayContainerContentSequence($attendee, $persistentExceptionEvent, Tinebase_Model_ContainerContent::ACTION_CREATE);
1492 // @todo save notes and add a update note -> what was updated? -> modlog is also missing
1493 $persistentExceptionEvent = $this->get($persistentExceptionEvent->getId());
1497 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " shorten recur series for/to: '{$_event->recurid}'");
1499 // split past/future exceptions
1500 $pastExdates = array();
1501 $futureExdates = array();
1502 foreach($exdates as $exdate) {
1503 $exdate->isLater($_event->dtstart) ? $futureExdates[] = $exdate : $pastExdates[] = $exdate;
1506 $persistentExceptionEvents = $this->getRecurExceptions($_event);
1507 $pastPersistentExceptionEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
1508 $futurePersistentExceptionEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
1509 foreach ($persistentExceptionEvents as $persistentExceptionEvent) {
1510 $persistentExceptionEvent->getOriginalDtStart()->isLater($_event->dtstart) ?
1511 $futurePersistentExceptionEvents->addRecord($persistentExceptionEvent) :
1512 $pastPersistentExceptionEvents->addRecord($persistentExceptionEvent);
1516 $rrule = Calendar_Model_Rrule::getRruleFromString($baseEvent->rrule);
1517 if (isset($rrule->count)) {
1518 // get all occurences and find the split
1520 $exdate = $baseEvent->exdate;
1521 $baseEvent->exdate = NULL;
1522 //$baseCountOccurrence = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $baseEvent->rrule_until, $baseCount);
1523 $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $baseEvent->dtstart, $baseEvent->rrule_until);
1524 $baseEvent->exdate = $exdate;
1526 $originalDtstart = $_event->getOriginalDtStart();
1527 foreach($recurSet as $idx => $rInstance) {
1528 if ($rInstance->dtstart >= $originalDtstart) break;
1531 $rrule->count = $idx+1;
1533 $lastBaseOccurence = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $_event->getOriginalDtStart()->subSecond(1), -1);
1534 $rrule->until = $lastBaseOccurence ? $lastBaseOccurence->getOriginalDtStart() : $baseEvent->dtstart;
1536 $baseEvent->rrule = (string) $rrule;
1537 $baseEvent->exdate = $pastExdates;
1539 // NOTE: we don't want implicit attendee updates
1540 //$updatedBaseEvent = $this->update($baseEvent, FALSE);
1541 $this->_inspectEvent($baseEvent);
1542 $updatedBaseEvent = parent::update($baseEvent);
1544 if ($_deleteInstance == TRUE) {
1545 // delete all future persistent events
1546 $this->delete($futurePersistentExceptionEvents->getId());
1548 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " create new recur series for/at: '{$_event->recurid}'");
1550 // NOTE: in order to move exceptions correctly in time we need to find out the original dtstart
1551 // and create the new baseEvent with this time. A following update also updates its exceptions
1552 $originalDtstart = new Tinebase_DateTime(substr($_event->recurid, -19));
1553 $adoptedDtstart = clone $_event->dtstart;
1554 $dtStartHasDiff = $adoptedDtstart->compare($originalDtstart) != 0; // php52 compat
1555 $eventLength = $_event->dtstart->diff($_event->dtend);
1557 $_event->dtstart = clone $originalDtstart;
1558 $_event->dtend = clone $originalDtstart;
1559 $_event->dtend->add($eventLength);
1562 if (isset($rrule->count)) {
1563 $baseCount = $rrule->count;
1564 $rrule = Calendar_Model_Rrule::getRruleFromString($_event->rrule);
1565 $rrule->count = $rrule->count - $baseCount;
1566 $_event->rrule = (string) $rrule;
1569 $_event->setId(Tinebase_Record_Abstract::generateUID());
1570 $_event->uid = $futurePersistentExceptionEvents->uid = Tinebase_Record_Abstract::generateUID();
1571 $_event->setId(Tinebase_Record_Abstract::generateUID());
1572 $futurePersistentExceptionEvents->setRecurId($_event->getId());
1573 unset($_event->recurid);
1574 unset($_event->base_event_id);
1575 foreach(array('attendee', 'notes', 'alarms') as $prop) {
1576 if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
1577 $_event->{$prop}->setId(NULL);
1580 $_event->exdate = $futureExdates;
1582 $attendees = $_event->attendee; unset($_event->attendee);
1583 $note = $_event->notes; unset($_event->notes);
1584 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts && $dtStartHasDiff);
1586 // we save attendee separately to preserve their attributes
1587 if ($attendees instanceof Tinebase_Record_RecordSet) {
1588 foreach($attendees as $attendee) {
1589 $this->_createAttender($attendee, $persistentExceptionEvent, true);
1593 // @todo save notes and add a update note -> what was updated? -> modlog is also missing
1595 $persistentExceptionEvent = $this->get($persistentExceptionEvent->getId());
1597 foreach($futurePersistentExceptionEvents as $futurePersistentExceptionEvent) {
1598 $this->update($futurePersistentExceptionEvent, FALSE);
1601 if ($dtStartHasDiff) {
1602 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " new recur series has adpted dtstart -> update to adopt exceptions'");
1603 $persistentExceptionEvent->dtstart = clone $adoptedDtstart;
1604 $persistentExceptionEvent->dtend = clone $adoptedDtstart;
1605 $persistentExceptionEvent->dtend->add($eventLength);
1607 $persistentExceptionEvent = $this->update($persistentExceptionEvent, $_checkBusyConflicts);
1612 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1614 // restore original notification handling
1615 $this->sendNotifications($sendNotifications);
1616 $notificationAction = $_deleteInstance ? 'deleted' : 'changed';
1617 $notificationEvent = $_deleteInstance ? $_event : $persistentExceptionEvent;
1620 $this->doContainerACLChecks($doContainerACLChecks);
1622 // send notifications
1623 if ($this->_sendNotifications && $_event->mute != 1) {
1624 // NOTE: recur exception is a fake event from client.
1625 // this might lead to problems, so we wrap the calls
1627 if (count($_event->attendee) > 0) {
1628 $_event->attendee->bypassFilters = TRUE;
1630 $_event->created_by = $baseEvent->created_by;
1632 $this->doSendNotifications($notificationEvent, Tinebase_Core::getUser(), $notificationAction, $originalEvent);
1633 } catch (Exception $e) {
1634 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
1635 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " could not send notification {$e->getMessage()}");
1639 return $_deleteInstance ? $updatedBaseEvent : $persistentExceptionEvent;
1644 * @see Tinebase_Controller_Record_Abstract::get()
1646 public function get($_id, $_containerId = NULL, $_getRelatedData = TRUE, $_getDeleted = FALSE)
1648 if (preg_match('/^fakeid(.*):(.*)/', $_id, $matches)) {
1649 $baseEvent = $this->get($matches[1]);
1650 $exceptions = $this->getRecurExceptions($baseEvent);
1651 $originalDtStart = new Tinebase_DateTime($matches[2]);
1653 $exdates = $exceptions->getOriginalDtStart();
1654 $exdate = array_search($originalDtStart, $exdates);
1656 return $exdate !== false ? $exceptions[$exdate] :
1657 Calendar_Model_Rrule::computeNextOccurrence($baseEvent, $exceptions, $originalDtStart);
1659 return parent::get($_id, $_containerId, $_getRelatedData, $_getDeleted);
1664 * returns base event of a recurring series
1666 * @param Calendar_Model_Event $_event
1667 * @return Calendar_Model_Event
1669 public function getRecurBaseEvent($_event)
1671 $baseEventId = $_event->base_event_id ?: $_event->id;
1673 if (! $baseEventId) {
1674 throw new Tinebase_Exception_NotFound('base event of a recurring series not found');
1677 // make sure we have a 'fully featured' event
1678 return $this->get($baseEventId);
1682 * returns all persistent recur exceptions of recur series
1684 * NOTE: deleted instances are saved in the base events exception property
1685 * NOTE: returns all exceptions regardless of current filters and access restrictions
1687 * @param Calendar_Model_Event $_event
1688 * @param boolean $_fakeDeletedInstances
1689 * @param Calendar_Model_EventFilter $_eventFilter
1690 * @return Tinebase_Record_RecordSet of Calendar_Model_Event
1692 public function getRecurExceptions($_event, $_fakeDeletedInstances = FALSE, $_eventFilter = NULL)
1694 $baseEventId = $_event->base_event_id ?: $_event->id;
1696 $exceptionFilter = new Calendar_Model_EventFilter(array(
1697 array('field' => 'base_event_id', 'operator' => 'equals', 'value' => $baseEventId),
1698 array('field' => 'recurid', 'operator' => 'notnull', 'value' => NULL)
1701 if ($_eventFilter instanceof Calendar_Model_EventFilter) {
1702 $exceptionFilter->addFilterGroup($_eventFilter);
1705 $exceptions = $this->_backend->search($exceptionFilter);
1707 if ($_fakeDeletedInstances) {
1708 $baseEvent = $this->getRecurBaseEvent($_event);
1709 $this->fakeDeletedExceptions($baseEvent, $exceptions);
1712 $exceptions->exdate = NULL;
1713 $exceptions->rrule = NULL;
1714 $exceptions->rrule_until = NULL;
1720 * add exceptions events for deleted instances
1722 * @param Calendar_Model_Event $baseEvent
1723 * @param Tinebase_Record_RecordSet $exceptions
1725 public function fakeDeletedExceptions($baseEvent, $exceptions)
1727 $eventLength = $baseEvent->dtstart->diff($baseEvent->dtend);
1729 // compute remaining exdates
1730 $deletedInstanceDtStarts = array_diff(array_unique((array) $baseEvent->exdate), $exceptions->getOriginalDtStart());
1732 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1733 ' Faking ' . count($deletedInstanceDtStarts) . ' deleted exceptions');
1735 foreach((array) $deletedInstanceDtStarts as $deletedInstanceDtStart) {
1736 $fakeEvent = clone $baseEvent;
1737 $fakeEvent->setId(NULL);
1739 $fakeEvent->dtstart = clone $deletedInstanceDtStart;
1740 $fakeEvent->dtend = clone $deletedInstanceDtStart;
1741 $fakeEvent->dtend->add($eventLength);
1742 $fakeEvent->is_deleted = TRUE;
1743 $fakeEvent->setRecurId($baseEvent->getId());
1744 $fakeEvent->rrule = null;
1746 $exceptions->addRecord($fakeEvent);
1751 * adopt alarm time to next occurrence for recurring events
1753 * @param Tinebase_Record_Abstract $_record
1754 * @param Tinebase_Model_Alarm $_alarm
1755 * @param bool $_nextBy {instance|time} set recurr alarm to next from given instance or next by current time
1758 public function adoptAlarmTime(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm, $_nextBy = 'time')
1760 if ($_record->rrule) {
1761 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1762 ' Adopting alarm time for next recur occurrence (by ' . $_nextBy . ')');
1763 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1764 ' ' . print_r($_record->toArray(), TRUE));
1766 if ($_nextBy == 'time') {
1767 // NOTE: this also finds instances running right now
1768 $from = Tinebase_DateTime::now();
1771 $recurid = $_alarm->getOption('recurid');
1772 $instanceStart = $recurid ? new Tinebase_DateTime(substr($recurid, -19)) : clone $_record->dtstart;
1773 $eventLength = $_record->dtstart->diff($_record->dtend);
1775 $instanceStart->setTimezone($_record->originator_tz);
1776 $from = $instanceStart->add($eventLength);
1777 $from->setTimezone('UTC');
1781 $exceptions = $this->getRecurExceptions($_record);
1782 $nextOccurrence = Calendar_Model_Rrule::computeNextOccurrence($_record, $exceptions, $from);
1784 if ($nextOccurrence === NULL) {
1785 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
1786 ' Recur series is over, no more alarms pending');
1788 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1789 ' Found next occurrence, adopting alarm to dtstart ' . $nextOccurrence->dtstart->toString());
1792 // save recurid so we know for which recurrance the alarm is for
1793 $_alarm->setOption('recurid', isset($nextOccurrence) ? $nextOccurrence->recurid : NULL);
1795 $_alarm->sent_status = $nextOccurrence ? Tinebase_Model_Alarm::STATUS_PENDING : Tinebase_Model_Alarm::STATUS_SUCCESS;
1796 $_alarm->sent_message = $nextOccurrence ? '' : 'Nothing to send, series is over';
1798 $eventStart = $nextOccurrence ? clone $nextOccurrence->dtstart : clone $_record->dtstart;
1800 $eventStart = clone $_record->dtstart;
1803 // save minutes before / compute it for custom alarms
1804 $minutesBefore = $_alarm->minutes_before == Tinebase_Model_Alarm::OPTION_CUSTOM
1805 ? ($_record->dtstart->getTimestamp() - $_alarm->alarm_time->getTimestamp()) / 60
1806 : $_alarm->minutes_before;
1807 $minutesBefore = round($minutesBefore);
1809 $_alarm->setOption('minutes_before', $minutesBefore);
1810 $_alarm->alarm_time = $eventStart->subMinute($minutesBefore);
1812 if ($_record->rrule && $_alarm->sent_status == Tinebase_Model_Alarm::STATUS_PENDING && $_alarm->alarm_time < $_alarm->sent_time) {
1813 $this->adoptAlarmTime($_record, $_alarm, 'instance');
1816 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1817 ' alarm: ' . print_r($_alarm->toArray(), true));
1820 /****************************** overwritten functions ************************/
1823 * restore original alarm time of recurring events
1825 * @param Tinebase_Record_Interface $_record
1828 protected function _inspectAlarmGet(Tinebase_Record_Interface $_record)
1830 foreach ($_record->alarms as $alarm) {
1831 if ($recurid = $alarm->getOption('recurid')) {
1832 $alarm->alarm_time = clone $_record->dtstart;
1833 $alarm->alarm_time->subMinute((int) $alarm->getOption('minutes_before'));
1837 parent::_inspectAlarmGet($_record);
1841 * adopt alarm time to next occurance for recurring events
1843 * @param Tinebase_Record_Interface $_record
1844 * @param Tinebase_Model_Alarm $_alarm
1845 * @param bool $_nextBy {instance|time} set recurr alarm to next from given instance or next by current time
1847 * @throws Tinebase_Exception_InvalidArgument
1849 protected function _inspectAlarmSet(Tinebase_Record_Interface $_record, Tinebase_Model_Alarm $_alarm, $_nextBy = 'time')
1851 parent::_inspectAlarmSet($_record, $_alarm);
1852 $this->adoptAlarmTime($_record, $_alarm, 'time');
1856 * inspect update of one record
1858 * @param Tinebase_Record_Interface $_record the update record
1859 * @param Tinebase_Record_Interface $_oldRecord the current persistent record
1862 protected function _inspectBeforeUpdate($_record, $_oldRecord)
1864 // if dtstart of an event changes, we update the originator_tz, alarm times
1865 if (! $_oldRecord->dtstart->equals($_record->dtstart)) {
1866 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' dtstart changed -> adopting organizer_tz');
1867 $_record->originator_tz = Tinebase_Core::getUserTimezone();
1868 if (! empty($_record->rrule)) {
1869 $diff = $_oldRecord->dtstart->diff($_record->dtstart);
1870 $this->_updateRecurIdOfExdates($_record, $diff);
1874 // delete recur exceptions if update is no longer a recur series
1875 if (! empty($_oldRecord->rrule) && empty($_record->rrule)) {
1876 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' deleting recur exceptions as event is no longer a recur series');
1877 $this->_backend->delete($this->getRecurExceptions($_record));
1880 // touch base event of a recur series if an persistent exception changes
1881 if ($_record->recurid) {
1882 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' touch base event of a persistent exception');
1883 $baseEvent = $this->getRecurBaseEvent($_record);
1884 $this->_touch($baseEvent, TRUE);
1887 if (empty($_record->recurid) && ! empty($_record->rrule) && (string)$_record->rrule == (string)$_oldRecord->rrule && $_record->dtstart->compare($_oldRecord->dtstart, 'Y-m-d') !== 0) {
1888 // we are in a base event & dtstart changed the date & the rrule was not changed
1889 // if easy rrule, try to adapt
1890 $rrule = $_record->rrule;
1891 if (! $rrule instanceof Calendar_Model_Rrule) {
1892 $rrule = new Calendar_Model_Rrule($rrule);
1894 $rrule = clone $rrule;
1897 switch($rrule->freq)
1899 case Calendar_Model_Rrule::FREQ_WEEKLY:
1900 // only do simple bydays
1901 if (empty($rrule->byday) || !isset(Calendar_Model_Rrule::$WEEKDAY_MAP[$rrule->byday])) {
1905 // check old dtstart matches byday, if not, we abort
1906 if (strtolower($_oldRecord->dtstart->format('D')) !== Calendar_Model_Rrule::$WEEKDAY_MAP[$rrule->byday]) {
1910 $rrule->byday = Calendar_Model_Rrule::$WEEKDAY_MAP_REVERSE[strtolower($_record->dtstart->format('D'))];
1911 $_record->rrule = $rrule;
1914 case Calendar_Model_Rrule::FREQ_MONTHLY:
1915 // if there is no day specification, nothing to do
1916 if (empty($rrule->byday) && empty($rrule->bymonthday)) {
1919 // only do simple rules
1920 if (!empty($rrule->byday) && (strpos(',', $rrule->byday) !== false)) {
1923 if (!empty($rrule->bymonthday) && strpos(',', $rrule->bymonthday) !== false) {
1926 if (!empty($rrule->byday) && !empty($rrule->bymonthday)) {
1930 if (!empty($rrule->byday)) {
1931 $bydayPrefix = intval($rrule->byday);
1932 $byday = substr($rrule->byday, -2);
1934 // if we dont have a quantifier we abort
1935 if ($bydayPrefix === 0) {
1939 // check old dtstart matches byday, if not we abort
1940 if (strtolower($_oldRecord->dtstart->format('D')) !== Calendar_Model_Rrule::$WEEKDAY_MAP[$byday]) {
1944 $dtstartJ = $_oldRecord->dtstart->format('j');
1945 // check old dtstart matches bydayPrefix, if not we abort
1946 if ($bydayPrefix === -1) {
1947 if ($_oldRecord->dtstart->format('t') - $dtstartJ > 6) {
1951 if ($dtstartJ - (($bydayPrefix-1)*7) > 6 || $dtstartJ - (($bydayPrefix-1)*7) < 1) {
1956 if ($_record->dtstart->format('j') > 28 || ($bydayPrefix === -1 && $_record->dtstart->format('t') - $_record->dtstart->format('j') < 7)) {
1957 // keep -1 => last X
1960 $prefix = floor(($_record->dtstart->format('j') - 1) / 7) + 1;
1963 $rrule->byday = $prefix . Calendar_Model_Rrule::$WEEKDAY_MAP_REVERSE[strtolower($_record->dtstart->format('D'))];
1964 $_record->rrule = $rrule;
1968 // check old dtstart matches bymonthday, if not we abort
1969 if ($_oldRecord->dtstart->format('j') != $rrule->bymonthday) {
1973 $rrule->bymonthday = $_record->dtstart->format('j');
1974 $_record->rrule = $rrule;
1979 case Calendar_Model_Rrule::FREQ_YEARLY:
1980 // only do simple rules
1981 if (! empty($rrule->byday) || empty($rrule->bymonth) || empty($rrule->bymonthday) || strpos($rrule->bymonth, ',') !== false ||
1982 strpos($rrule->bymonthday, ',') !== false ||
1983 // check old dtstart matches the date
1984 $_oldRecord->dtstart->format('j') != $rrule->bymonthday || $_oldRecord->dtstart->format('n') != $rrule->bymonth
1989 $rrule->bymonthday = $_record->dtstart->format('j');
1990 $rrule->bymonth = $_record->dtstart->format('n');
1991 $_record->rrule = $rrule;
1999 * update exdates and recurids if dtstart of an recurevent changes
2001 * @param Calendar_Model_Event $_record
2002 * @param DateInterval $diff
2004 protected function _updateRecurIdOfExdates($_record, $diff)
2006 // update exceptions
2007 $exceptions = $this->getRecurExceptions($_record);
2008 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' dtstart of a series changed -> adopting '. count($exceptions) . ' recurid(s)');
2010 foreach ($exceptions as $exception) {
2011 $exception->recurid = new Tinebase_DateTime(substr($exception->recurid, -19));
2012 Calendar_Model_Rrule::addUTCDateDstFix($exception->recurid, $diff, $_record->originator_tz);
2013 $exdates[] = $exception->recurid;
2015 $exception->setRecurId($_record->getId());
2016 $this->_backend->update($exception);
2019 $_record->exdate = $exdates;
2023 * inspect before create/update
2025 * @TODO move stuff from other places here
2026 * @param Calendar_Model_Event $_record the record to inspect
2028 protected function _inspectEvent($_record, $skipEvent = false)
2030 $_record->uid = $_record->uid ? $_record->uid : Tinebase_Record_Abstract::generateUID();
2031 $_record->organizer = $_record->organizer ? $_record->organizer : Tinebase_Core::getUser()->contact_id;
2032 $_record->transp = $_record->transp ? $_record->transp : Calendar_Model_Event::TRANSP_OPAQUE;
2034 $this->_inspectOriginatorTZ($_record);
2036 if ($_record->hasExternalOrganizer()) {
2037 // assert calendarUser as attendee. This is important to keep the event in the loop via its displaycontianer(s)
2039 $container = Tinebase_Container::getInstance()->getContainerById($_record->container_id);
2040 $owner = $container->getOwner();
2041 $calendarUserId = Addressbook_Controller_Contact::getInstance()->getContactByUserId($owner, true)->getId();
2042 } catch (Exception $e) {
2044 $calendarUserId = Tinebase_Core::getUser()->contact_id;
2047 $attendee = $_record->assertAttendee(new Calendar_Model_Attender(array(
2048 'user_type' => Calendar_Model_Attender::USERTYPE_USER,
2049 'user_id' => $calendarUserId
2050 )), false, false, true);
2052 if ($attendee && $container instanceof Tinebase_Model_Container) {
2053 $attendee->displaycontainer_id = $container->getId();
2056 if (! $container instanceof Tinebase_Model_Container || $container->type == Tinebase_Model_Container::TYPE_PERSONAL) {
2057 // move into special (external users) container
2058 $container = Calendar_Controller::getInstance()->getInvitationContainer($_record->resolveOrganizer());
2059 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2060 . ' Setting container_id to ' . $container->getId() . ' for external organizer ' . $_record->organizer->email);
2061 $_record->container_id = $container->getId();
2066 if ($_record->is_all_day_event) {
2067 // harmonize datetimes of all day events
2068 $_record->setTimezone($_record->originator_tz);
2069 if (! $_record->dtend) {
2070 $_record->dtend = clone $_record->dtstart;
2071 $_record->dtend->setTime(23,59,59);
2073 $_record->dtstart->setTime(0,0,0);
2074 $_record->dtend->setTime(23,59,59);
2075 $_record->setTimezone('UTC');
2077 $_record->setRruleUntil();
2079 if ($_record->rrule instanceof Calendar_Model_Rrule) {
2080 $_record->rrule->normalize($_record);
2083 if ($_record->isRecurException()) {
2084 $_record->rrule = NULL;
2085 $_record->rrule_constraints = NULL;
2088 // $baseEvent = $this->getRecurBaseEvent($_record);
2089 // // exdates needs to stay in baseEvents container
2090 // if($_record->container_id != $baseEvent->container_id) {
2091 // throw new Calendar_Exception_ExdateContainer();
2095 // inspect rrule_constraints
2096 if ($_record->rrule_constraints) {
2097 $this->setConstraintsExdates($_record);
2101 Tinebase_Record_PersistentObserver::getInstance()->fireEvent(new Calendar_Event_InspectEvent(array(
2102 'observable' => $_record
2108 * checks/sets originator timezone
2111 * @throws Tinebase_Exception_Record_Validation
2113 protected function _inspectOriginatorTZ($record)
2115 $record->originator_tz = $record->originator_tz ? $record->originator_tz : Tinebase_Core::getUserTimezone();
2118 new DateTimeZone($record->originator_tz);
2119 } catch (Exception $e) {
2120 throw new Tinebase_Exception_Record_Validation('Bad Timezone: ' . $record->originator_tz);
2125 * inspects delete action
2127 * @param array $_ids
2128 * @return array of ids to actually delete
2130 protected function _inspectDelete(array $_ids) {
2131 $events = $this->_backend->getMultiple($_ids);
2133 foreach ($events as $event) {
2135 // implicitly delete persistent recur instances of series
2136 if (! empty($event->rrule)) {
2137 $exceptionIds = $this->getRecurExceptions($event)->getId();
2138 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2139 . ' Implicitly deleting ' . (count($exceptionIds) - 1 ) . ' persistent exception(s) for recurring series with uid' . $event->uid);
2140 $_ids = array_merge($_ids, $exceptionIds);
2144 $this->_deleteAlarmsForIds($_ids);
2146 return array_unique($_ids);
2150 * redefine required grants for get actions
2152 * @param Tinebase_Model_Filter_FilterGroup $_filter
2153 * @param string $_action get|update
2155 public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
2157 $hasGrantsFilter = FALSE;
2158 foreach($_filter->getAclFilters() as $aclFilter) {
2159 if ($aclFilter instanceof Calendar_Model_GrantFilter) {
2160 $hasGrantsFilter = TRUE;
2165 if (! $hasGrantsFilter && $this->_doContainerACLChecks) {
2166 // force a grant filter
2167 // NOTE: actual grants are set via setRequiredGrants later
2168 $grantsFilter = $_filter->createFilter('grants', 'in', '@setRequiredGrants');
2169 $_filter->addFilter($grantsFilter);
2172 parent::checkFilterACL($_filter, $_action);
2174 if ($_action == 'get') {
2175 $_filter->setRequiredGrants(array(
2176 Tinebase_Model_Grants::GRANT_FREEBUSY,
2177 Tinebase_Model_Grants::GRANT_READ,
2178 Tinebase_Model_Grants::GRANT_ADMIN,
2184 * check grant for action (CRUD)
2186 * @param Tinebase_Record_Interface $_record
2187 * @param string $_action
2188 * @param boolean $_throw
2189 * @param string $_errorMessage
2190 * @param Tinebase_Record_Interface $_oldRecord
2192 * @throws Tinebase_Exception_AccessDenied
2194 * @todo use this function in other create + update functions
2195 * @todo invent concept for simple adding of grants (plugins?)
2197 protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL)
2199 if ( ! $this->_doContainerACLChecks
2200 // admin grant includes all others (only if class is PUBLIC)
2201 || (! empty($this->class) && $this->class === Calendar_Model_Event::CLASS_PUBLIC
2202 && $_record->container_id && Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADMIN))
2203 // external invitations are in a spechial invitaion calendar. only attendee can see it via displaycal
2204 || $_record->hasExternalOrganizer()
2211 // NOTE: free/busy is not a read grant!
2212 $hasGrant = $_record->hasGrant(Tinebase_Model_Grants::GRANT_READ);
2214 $_record->doFreeBusyCleanup();
2218 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD);
2221 $hasGrant = (bool) $_oldRecord->hasGrant(Tinebase_Model_Grants::GRANT_EDIT);
2223 if ($_oldRecord->container_id != $_record->container_id) {
2224 $hasGrant &= Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD)
2225 && $_oldRecord->hasGrant(Tinebase_Model_Grants::GRANT_DELETE);
2229 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_DELETE);
2232 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_SYNC);
2235 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_EXPORT);
2241 throw new Tinebase_Exception_AccessDenied($_errorMessage);
2243 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2244 . ' No permissions to ' . $_action . ' in container ' . $_record->container_id);
2252 * touches (sets seq, last_modified_time and container content sequence) given event
2257 protected function _touch($_event, $_setModifier = FALSE)
2259 $_event->last_modified_time = Tinebase_DateTime::now();
2260 $_event->seq = (int)$_event->seq + 1;
2261 if ($_setModifier) {
2262 $_event->last_modified_by = Tinebase_Core::getUser()->getId();
2265 $this->_backend->update($_event);
2267 $this->_increaseContainerContentSequence($_event, Tinebase_Model_ContainerContent::ACTION_UPDATE);
2271 * increase container content sequence
2273 * @param Tinebase_Record_Interface $_record
2274 * @param string $action
2276 protected function _increaseContainerContentSequence(Tinebase_Record_Interface $record, $action = NULL)
2278 parent::_increaseContainerContentSequence($record, $action);
2280 if ($record->attendee instanceof Tinebase_Record_RecordSet) {
2281 $updatedContainerIds = array($record->container_id);
2282 foreach ($record->attendee as $attender) {
2283 if (isset($attender->displaycontainer_id) && ! in_array($attender->displaycontainer_id, $updatedContainerIds)) {
2284 Tinebase_Container::getInstance()->increaseContentSequence($attender->displaycontainer_id, $action, $record->getId());
2285 $updatedContainerIds[] = $attender->displaycontainer_id;
2292 /****************************** attendee functions ************************/
2295 * creates an attender status exception of a recurring event series
2297 * NOTE: Recur exceptions are implicitly created
2299 * @param Calendar_Model_Event $_recurInstance
2300 * @param Calendar_Model_Attender $_attender
2301 * @param string $_authKey
2302 * @param bool $_allFollowing
2303 * @return Calendar_Model_Attender updated attender
2305 public function attenderStatusCreateRecurException($_recurInstance, $_attender, $_authKey, $_allFollowing = FALSE)
2308 $db = $this->_backend->getAdapter();
2309 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
2311 $baseEvent = $this->getRecurBaseEvent($_recurInstance);
2312 $baseEventAttendee = Calendar_Model_Attender::getAttendee($baseEvent->attendee, $_attender);
2314 if ($baseEvent->getId() == $_recurInstance->getId()) {
2315 // exception to the first occurence
2316 $_recurInstance->setRecurId($baseEvent->getId());
2319 // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
2320 if (empty($_recurInstance->recurid)) {
2321 throw new Exception('recurid must be present to create exceptions!');
2325 // check if we already have a persistent exception for this event
2326 $eventInstance = $this->_backend->getByProperty($_recurInstance->recurid, $_property = 'recurid');
2328 // NOTE: the user must exist (added by someone with appropriate rights by createRecurException)
2329 $exceptionAttender = Calendar_Model_Attender::getAttendee($eventInstance->attendee, $_attender);
2330 if (! $exceptionAttender) {
2331 throw new Tinebase_Exception_AccessDenied('not an attendee');
2335 if ($exceptionAttender->status_authkey != $_authKey) {
2336 // NOTE: it might happen, that the user set her status from the base event without knowing about
2337 // an existing exception. In this case the base event authkey is also valid
2338 if (! $baseEventAttendee || $baseEventAttendee->status_authkey != $_authKey) {
2339 throw new Tinebase_Exception_AccessDenied('Attender authkey mismatch');
2343 } catch (Tinebase_Exception_NotFound $e) {
2344 // otherwise create it implicilty
2346 // check if this intance takes place
2347 if (in_array($_recurInstance->dtstart, (array)$baseEvent->exdate)) {
2348 throw new Tinebase_Exception_AccessDenied('Event instance is deleted and may not be recreated via status setting!');
2351 if (! $baseEventAttendee) {
2352 throw new Tinebase_Exception_AccessDenied('not an attendee');
2355 if ($baseEventAttendee->status_authkey != $_authKey) {
2356 throw new Tinebase_Exception_AccessDenied('Attender authkey mismatch');
2359 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " creating recur exception for a exceptional attendee status");
2361 $doContainerAclChecks = $this->doContainerACLChecks(FALSE);
2362 $sendNotifications = $this->sendNotifications(FALSE);
2364 // NOTE: the user might have no edit grants, so let's be carefull
2365 $diff = $baseEvent->dtstart->diff($baseEvent->dtend);
2367 $baseEvent->dtstart = new Tinebase_DateTime(substr($_recurInstance->recurid, -19), 'UTC');
2368 $baseEvent->dtend = clone $baseEvent->dtstart;
2369 $baseEvent->dtend->add($diff);
2371 $baseEvent->base_event_id = $baseEvent->id;
2372 $baseEvent->id = $_recurInstance->id;
2373 $baseEvent->recurid = $_recurInstance->recurid;
2375 $attendee = $baseEvent->attendee;
2376 unset($baseEvent->attendee);
2378 $eventInstance = $this->createRecurException($baseEvent, FALSE, $_allFollowing);
2379 $eventInstance->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
2380 $this->doContainerACLChecks($doContainerAclChecks);
2381 $this->sendNotifications($sendNotifications);
2383 foreach ($attendee as $attender) {
2384 $attender->setId(NULL);
2385 $attender->cal_event_id = $eventInstance->getId();
2387 $attender = $this->_backend->createAttendee($attender);
2388 $eventInstance->attendee->addRecord($attender);
2389 $this->_increaseDisplayContainerContentSequence($attender, $eventInstance, Tinebase_Model_ContainerContent::ACTION_CREATE);
2392 $exceptionAttender = Calendar_Model_Attender::getAttendee($eventInstance->attendee, $_attender);
2395 $exceptionAttender->status = $_attender->status;
2396 $exceptionAttender->transp = $_attender->transp;
2397 $eventInstance->alarms = clone $_recurInstance->alarms;
2398 $eventInstance->alarms->setId(NULL);
2400 $updatedAttender = $this->attenderStatusUpdate($eventInstance, $exceptionAttender, $exceptionAttender->status_authkey);
2402 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
2403 } catch (Exception $e) {
2404 Tinebase_TransactionManager::getInstance()->rollBack();
2408 return $updatedAttender;
2412 * updates an attender status of a event
2414 * @param Calendar_Model_Event $_event
2415 * @param Calendar_Model_Attender $_attender
2416 * @param string $_authKey
2417 * @return Calendar_Model_Attender updated attender
2419 public function attenderStatusUpdate(Calendar_Model_Event $_event, Calendar_Model_Attender $_attender, $_authKey)
2422 $event = $this->get($_event->getId());
2424 if (! $event->attendee) {
2425 throw new Tinebase_Exception_NotFound('Could not find any attendee of event.');
2428 if (($currentAttender = Calendar_Model_Attender::getAttendee($event->attendee, $_attender)) == null) {
2429 throw new Tinebase_Exception_NotFound('Could not find attender in event.');
2432 $updatedAttender = clone $currentAttender;
2434 if ($currentAttender->status_authkey !== $_authKey) {
2435 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
2436 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " no permissions to update status for {$currentAttender->user_type} {$currentAttender->user_id}");
2437 return $updatedAttender;
2440 Calendar_Controller_Alarm::enforceACL($_event, $event);
2442 $currentAttenderDisplayContainerId = $currentAttender->displaycontainer_id instanceof Tinebase_Model_Container ?
2443 $currentAttender->displaycontainer_id->getId() :
2444 $currentAttender->displaycontainer_id;
2446 $attenderDisplayContainerId = $_attender->displaycontainer_id instanceof Tinebase_Model_Container ?
2447 $_attender->displaycontainer_id->getId() :
2448 $_attender->displaycontainer_id;
2450 // check if something what can be set as user has changed
2451 if ($currentAttender->status == $_attender->status &&
2452 $currentAttenderDisplayContainerId == $attenderDisplayContainerId &&
2453 $currentAttender->transp == $_attender->transp &&
2454 ! Calendar_Controller_Alarm::hasUpdates($_event, $event)
2456 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
2457 Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . "no status change -> do nothing");
2458 return $updatedAttender;
2461 $updatedAttender->status = $_attender->status;
2462 $updatedAttender->displaycontainer_id = isset($_attender->displaycontainer_id) ? $_attender->displaycontainer_id : $updatedAttender->displaycontainer_id;
2463 $updatedAttender->transp = isset($_attender->transp) ? $_attender->transp : Calendar_Model_Event::TRANSP_OPAQUE;
2465 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
2466 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2467 . " update attender status to {$_attender->status} for {$currentAttender->user_type} {$currentAttender->user_id}");
2468 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2469 . ' set alarm_ack_time / alarm_snooze_time: ' . $updatedAttender->alarm_ack_time . ' / ' . $updatedAttender->alarm_snooze_time);
2472 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
2474 $updatedAttender = $this->_backend->updateAttendee($updatedAttender);
2475 if ($_event->alarms instanceof Tinebase_Record_RecordSet) {
2476 foreach($_event->alarms as $alarm) {
2477 $this->_inspectAlarmSet($event, $alarm);
2480 Tinebase_Alarm::getInstance()->setAlarmsOfRecord($_event);
2483 $event->attendee->removeRecord($currentAttender);
2484 $event->attendee->addRecord($updatedAttender);
2486 $this->_increaseDisplayContainerContentSequence($updatedAttender, $event);
2488 Tinebase_Record_PersistentObserver::getInstance()->fireEvent(new Calendar_Event_InspectEvent(array(
2489 'observable' => $event
2492 $this->_touch($event, true);
2494 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
2495 } catch (Exception $e) {
2496 Tinebase_TransactionManager::getInstance()->rollBack();
2500 // send notifications
2501 if ($currentAttender->status != $updatedAttender->status && $this->_sendNotifications && $_event->mute != 1) {
2502 $updatedEvent = $this->get($event->getId());
2503 $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
2506 return $updatedAttender;
2510 * saves all attendee of given event
2512 * NOTE: This function is executed in a create/update context. As such the user
2513 * has edit/update the event and can do anything besides status settings of attendee
2515 * @todo add support for resources
2517 * @param Calendar_Model_Event $_event
2518 * @param Calendar_Model_Event $_currentEvent
2519 * @param bool $_isRescheduled event got rescheduled reset all attendee status
2521 protected function _saveAttendee($_event, $_currentEvent = NULL, $_isRescheduled = FALSE)
2523 if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
2524 $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
2527 Calendar_Model_Attender::resolveEmailOnlyAttendee($_event);
2529 $_event->attendee->cal_event_id = $_event->getId();
2531 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " About to save attendee for event {$_event->id} ");
2533 $currentAttendee = $_currentEvent->attendee;
2535 $diff = $currentAttendee->getMigration($_event->attendee->getArrayOfIds());
2537 $calendar = Tinebase_Container::getInstance()->getContainerById($_event->container_id);
2540 $this->_backend->deleteAttendee($diff['toDeleteIds']);
2541 foreach ($diff['toDeleteIds'] as $deleteAttenderId) {
2542 $idx = $currentAttendee->getIndexById($deleteAttenderId);
2543 if ($idx !== FALSE) {
2544 $currentAttenderToDelete = $currentAttendee[$idx];
2545 $this->_increaseDisplayContainerContentSequence($currentAttenderToDelete, $_event, Tinebase_Model_ContainerContent::ACTION_DELETE);
2549 // create/update attendee
2550 foreach ($_event->attendee as $attender) {
2551 $attenderId = $attender->getId();
2552 $idx = ($attenderId) ? $currentAttendee->getIndexById($attenderId) : FALSE;
2554 if ($idx !== FALSE) {
2555 $currentAttender = $currentAttendee[$idx];
2556 $this->_updateAttender($attender, $currentAttender, $_event, $_isRescheduled, $calendar);
2558 $this->_createAttender($attender, $_event, FALSE, $calendar);
2564 * creates a new attender
2566 * @param Calendar_Model_Attender $attender
2567 * @param Tinebase_Model_Container $_calendar
2568 * @param boolean $preserveStatus
2569 * @param Tinebase_Model_Container $calendar
2571 protected function _createAttender(Calendar_Model_Attender $attender, Calendar_Model_Event $event, $preserveStatus = FALSE, Tinebase_Model_Container $calendar = NULL)
2574 $attender->user_type = isset($attender->user_type) ? $attender->user_type : Calendar_Model_Attender::USERTYPE_USER;
2575 $attender->cal_event_id = $event->getId();
2576 $calendar = ($calendar) ? $calendar : Tinebase_Container::getInstance()->getContainerById($event->container_id);
2578 $userAccountId = $attender->getUserAccountId();
2580 // generate auth key
2581 if (! $attender->status_authkey) {
2582 $attender->status_authkey = Tinebase_Record_Abstract::generateUID();
2585 // attach to display calendar if attender has/is a useraccount
2586 if ($userAccountId) {
2587 if ($calendar->type == Tinebase_Model_Container::TYPE_PERSONAL && Tinebase_Container::getInstance()->hasGrant($userAccountId, $calendar, Tinebase_Model_Grants::GRANT_ADMIN)) {
2588 // if attender has admin grant to (is owner of) personal physical container, this phys. cal also gets displ. cal
2589 $attender->displaycontainer_id = $calendar->getId();
2590 } else if ($attender->displaycontainer_id && $userAccountId == Tinebase_Core::getUser()->getId() && Tinebase_Container::getInstance()->hasGrant($userAccountId, $attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
2591 // allow user to set his own displ. cal
2592 $attender->displaycontainer_id = $attender->displaycontainer_id;
2594 $displayCalId = self::getDefaultDisplayContainerId($userAccountId);
2595 $attender->displaycontainer_id = $displayCalId;
2598 } else if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
2599 $resource = Calendar_Controller_Resource::getInstance()->get($attender->user_id);
2600 $attender->displaycontainer_id = $resource->container_id;
2603 if ($attender->displaycontainer_id) {
2604 // check if user is allowed to set status
2605 if (! $preserveStatus && ! Tinebase_Core::getUser()->hasGrant($attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_EDIT)) {
2606 if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
2607 //If resource has an default status use this
2608 $attender->status = isset($resource->status) ? $resource->status : Calendar_Model_Attender::STATUS_NEEDSACTION;
2610 $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
2615 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " New attender: " . print_r($attender->toArray(), TRUE));
2617 Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($attender, 'create');
2618 $this->_backend->createAttendee($attender);
2619 $this->_increaseDisplayContainerContentSequence($attender, $event, Tinebase_Model_ContainerContent::ACTION_CREATE);
2624 * returns the default calendar
2626 * @return Tinebase_Model_Container
2628 public function getDefaultCalendar()
2630 return Tinebase_Container::getInstance()->getDefaultContainer($this->_applicationName, NULL, Calendar_Preference::DEFAULTCALENDAR);
2634 * returns default displayContainer id of given attendee
2636 * @param string $userAccountId
2638 public static function getDefaultDisplayContainerId($userAccountId)
2640 $userAccountId = Tinebase_Model_User::convertUserIdToInt($userAccountId);
2641 $displayCalId = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::DEFAULTCALENDAR, $userAccountId);
2644 // assert that displaycal is of type personal
2645 $container = Tinebase_Container::getInstance()->getContainerById($displayCalId);
2646 if ($container->type != Tinebase_Model_Container::TYPE_PERSONAL) {
2647 $displayCalId = NULL;
2649 } catch (Exception $e) {
2650 $displayCalId = NULL;
2653 if (! isset($displayCalId)) {
2654 $containers = Tinebase_Container::getInstance()->getPersonalContainer($userAccountId, 'Calendar_Model_Event', $userAccountId, 0, true);
2655 if ($containers->count() > 0) {
2656 $displayCalId = $containers->getFirstRecord()->getId();
2660 return $displayCalId;
2664 * increases content sequence of attender display container
2666 * @param Calendar_Model_Attender $attender
2667 * @param Calendar_Model_Event $event
2668 * @param string $action
2670 protected function _increaseDisplayContainerContentSequence($attender, $event, $action = Tinebase_Model_ContainerContent::ACTION_UPDATE)
2672 if ($event->container_id === $attender->displaycontainer_id || empty($attender->displaycontainer_id)) {
2673 // no need to increase sequence
2677 Tinebase_Container::getInstance()->increaseContentSequence($attender->displaycontainer_id, $action, $event->getId());
2681 * updates an attender
2683 * @param Calendar_Model_Attender $attender
2684 * @param Calendar_Model_Attender $currentAttender
2685 * @param Calendar_Model_Event $event
2686 * @param bool $isRescheduled event got rescheduled reset all attendee status
2687 * @param Tinebase_Model_Container $calendar
2689 protected function _updateAttender($attender, $currentAttender, $event, $isRescheduled, $calendar = NULL)
2691 $userAccountId = $currentAttender->getUserAccountId();
2693 // update display calendar if attender has/is a useraccount
2694 if ($userAccountId) {
2695 if ($calendar->type == Tinebase_Model_Container::TYPE_PERSONAL && Tinebase_Container::getInstance()->hasGrant($userAccountId, $calendar, Tinebase_Model_Grants::GRANT_ADMIN)) {
2696 // if attender has admin grant to personal physical container, this phys. cal also gets displ. cal
2697 $attender->displaycontainer_id = $calendar->getId();
2698 } else if ($userAccountId == Tinebase_Core::getUser()->getId() && Tinebase_Container::getInstance()->hasGrant($userAccountId, $attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
2699 // allow user to set his own displ. cal
2700 $attender->displaycontainer_id = $attender->displaycontainer_id;
2702 $attender->displaycontainer_id = $currentAttender->displaycontainer_id;
2706 // reset status if user has no right and authkey is wrong
2707 if ($attender->displaycontainer_id) {
2708 if (! Tinebase_Core::getUser()->hasGrant($attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_EDIT)
2709 && $attender->status_authkey != $currentAttender->status_authkey) {
2711 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
2712 . ' Wrong authkey, resetting status (' . $attender->status . ' -> ' . $currentAttender->status . ')');
2713 $attender->status = $currentAttender->status;
2717 // reset all status but calUser on reschedule except resources (Resources might have a configured default value)
2718 if ($isRescheduled && !$attender->isSame($this->getCalendarUser())) {
2719 if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
2720 //If resource has a default status reset to this
2721 $resource = Calendar_Controller_Resource::getInstance()->get($attender->user_id);
2722 $attender->status = isset($resource->status) ? $resource->status : Calendar_Model_Attender::STATUS_NEEDSACTION;
2724 $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
2726 $attender->transp = null;
2729 // preserve old authkey
2730 $attender->status_authkey = $currentAttender->status_authkey;
2732 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
2733 . " Updating attender: " . print_r($attender->toArray(), TRUE));
2736 Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($attender, 'update', $currentAttender);
2737 Tinebase_Timemachine_ModificationLog::getInstance()->writeModLog($attender, $currentAttender, get_class($attender), $this->_getBackendType(), $attender->getId());
2738 $this->_backend->updateAttendee($attender);
2740 if ($attender->displaycontainer_id !== $currentAttender->displaycontainer_id) {
2741 $this->_increaseDisplayContainerContentSequence($currentAttender, $event, Tinebase_Model_ContainerContent::ACTION_DELETE);
2742 $this->_increaseDisplayContainerContentSequence($attender, $event, Tinebase_Model_ContainerContent::ACTION_CREATE);
2744 $this->_increaseDisplayContainerContentSequence($attender, $event);
2749 * event handler for group updates
2751 * @param Tinebase_Model_Group $_group
2754 public function onUpdateGroup($_groupId)
2756 $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
2758 $filter = new Calendar_Model_EventFilter(array(
2759 array('field' => 'attender', 'operator' => 'equals', 'value' => array(
2760 'user_type' => Calendar_Model_Attender::USERTYPE_GROUP,
2761 'user_id' => $_groupId
2763 array('field' => 'period', 'operator' => 'within', 'value' => array(
2764 'from' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
2765 'until' => Tinebase_DateTime::now()->addYear(100)->get(Tinebase_Record_Abstract::ISO8601LONG))
2768 $events = $this->search($filter, new Tinebase_Model_Pagination(), FALSE, FALSE);
2770 foreach($events as $event) {
2772 if (! $event->rrule) {
2773 // update non recurring futrue events
2774 Calendar_Model_Attender::resolveGroupMembers($event->attendee);
2775 $this->update($event);
2777 // update thisandfuture for recurring events
2778 $nextOccurrence = Calendar_Model_Rrule::computeNextOccurrence($event, $this->getRecurExceptions($event), Tinebase_DateTime::now());
2779 Calendar_Model_Attender::resolveGroupMembers($nextOccurrence->attendee);
2781 if ($nextOccurrence->dtstart != $event->dtstart) {
2782 $this->createRecurException($nextOccurrence, FALSE, TRUE);
2784 $this->update($nextOccurrence);
2787 } catch (Exception $e) {
2788 Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " could not update attendee");
2792 $this->doContainerACLChecks($doContainerACLChecks);
2795 /****************************** alarm functions ************************/
2800 * @param Tinebase_Model_Alarm $_alarm
2803 * NOTE: the given alarm is raw and has not passed _inspectAlarmGet
2805 * @todo throw exception on error
2807 public function sendAlarm(Tinebase_Model_Alarm $_alarm)
2809 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " About to send alarm " . print_r($_alarm->toArray(), TRUE));
2811 $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
2814 $event = $this->get($_alarm->record_id);
2815 $event->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm', array($_alarm));
2816 $this->_inspectAlarmGet($event);
2817 } catch (Exception $e) {
2818 $this->doContainerACLChecks($doContainerACLChecks);
2822 $this->doContainerACLChecks($doContainerACLChecks);
2824 if ($event->rrule) {
2825 $recurid = $_alarm->getOption('recurid');
2827 // adopts the (referenced) alarm and sets alarm time to next occurance
2828 parent::_inspectAlarmSet($event, $_alarm);
2829 $this->adoptAlarmTime($event, $_alarm, 'instance');
2831 // sent_status might have changed in adoptAlarmTime()
2832 if ($_alarm->sent_status !== Tinebase_Model_Alarm::STATUS_PENDING) {
2833 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2834 . ' Not sending alarm for event at ' . $event->dtstart->toString() . ' with status ' . $_alarm->sent_status);
2839 // NOTE: In case of recuring events $event is always the baseEvent,
2840 // so we might need to adopt event time to recur instance.
2841 $diff = $event->dtstart->diff($event->dtend);
2843 $event->dtstart = new Tinebase_DateTime(substr($recurid, -19));
2845 $event->dtend = clone $event->dtstart;
2846 $event->dtend->add($diff);
2849 if ($event->exdate && in_array($event->dtstart, $event->exdate)) {
2850 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2851 . " Not sending alarm because instance at " . $event->dtstart->toString() . ' is an exception.');
2856 Calendar_Controller_EventNotifications::getInstance()->doSendNotifications($event, Tinebase_Core::getUser(), 'alarm', NULL, array('alarm' => $_alarm));
2860 * send notifications
2862 * @param Tinebase_Record_Interface $_event
2863 * @param Tinebase_Model_FullUser $_updater
2864 * @param String $_action
2865 * @param Tinebase_Record_Interface $_oldEvent
2866 * @param Array $_additionalData
2868 public function doSendNotifications(Tinebase_Record_Interface $_event, Tinebase_Model_FullUser $_updater, $_action, Tinebase_Record_Interface $_oldEvent = NULL, array $_additionalData = array())
2870 Tinebase_ActionQueue::getInstance()->queueAction('Calendar.sendEventNotifications',
2874 $_oldEvent ? $_oldEvent : NULL
2878 public function compareCalendars($cal1, $cal2, $from, $until)
2880 $matchingEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2881 $changedEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2882 $missingEventsInCal1 = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2883 $missingEventsInCal2 = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2884 $cal2EventIdsAlreadyProcessed = array();
2886 while ($from->isEarlier($until)) {
2888 $endWeek = $from->getClone()->addWeek(1);
2890 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2891 . ' Comparing period ' . $from . ' - ' . $endWeek);
2893 // get all events from cal1+cal2 for the week
2894 $cal1Events = $this->_getEventsForPeriodAndCalendar($cal1, $from, $endWeek);
2895 $cal1EventsClone = clone $cal1Events;
2896 $cal2Events = $this->_getEventsForPeriodAndCalendar($cal2, $from, $endWeek);
2897 $cal2EventsClone = clone $cal2Events;
2900 if (count($cal1Events) == 0 && count($cal2Events) == 0) {
2901 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2902 . ' No events found');
2906 foreach ($cal1Events as $event) {
2907 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2908 . ' Checking event "' . $event->summary . '" ' . $event->dtstart . ' - ' . $event->dtend);
2910 if ($event->container_id != $cal1) {
2911 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2912 . ' Event is in another calendar - skip');
2913 $cal1Events->removeRecord($event);
2917 $summaryMatch = $cal2Events->filter('summary', $event->summary);
2918 if (count($summaryMatch) > 0) {
2919 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2920 . " Found " . count($summaryMatch) . ' events with matching summaries');
2922 $dtStartMatch = $summaryMatch->filter('dtstart', $event->dtstart);
2923 if (count($dtStartMatch) > 0) {
2924 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2925 . " Found " . count($summaryMatch) . ' events with matching dtstarts and summaries');
2927 $matchingEvents->merge($dtStartMatch);
2928 // remove from cal1+cal2
2929 $cal1Events->removeRecord($event);
2930 $cal2Events->removeRecords($dtStartMatch);
2931 $cal2EventIdsAlreadyProcessed = array_merge($cal2EventIdsAlreadyProcessed, $dtStartMatch->getArrayOfIds());
2933 $changedEvents->merge($summaryMatch);
2934 $cal1Events->removeRecord($event);
2935 $cal2Events->removeRecords($summaryMatch);
2936 $cal2EventIdsAlreadyProcessed = array_merge($cal2EventIdsAlreadyProcessed, $summaryMatch->getArrayOfIds());
2941 // add missing events
2942 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2943 . " Found " . count($cal1Events) . ' events missing in cal2');
2944 $missingEventsInCal2->merge($cal1Events);
2946 // compare cal2 -> cal1 and add events as missing from cal1 that we did not detect before
2947 foreach ($cal2EventsClone as $event) {
2948 if (in_array($event->getId(), $cal2EventIdsAlreadyProcessed)) {
2951 if ($event->container_id != $cal2) {
2952 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2953 . ' Event is in another calendar - skip');
2957 $missingEventsInCal1->addRecord($event);
2962 'matching' => $matchingEvents,
2963 'changed' => $changedEvents,
2964 'missingInCal1' => $missingEventsInCal1,
2965 'missingInCal2' => $missingEventsInCal2,
2970 protected function _getEventsForPeriodAndCalendar($calendarId, $from, $until)
2972 $filter = new Calendar_Model_EventFilter(array(
2973 array('field' => 'period', 'operator' => 'within', 'value' =>
2974 array("from" => $from, "until" => $until)
2976 array('field' => 'container_id', 'operator' => 'equals', 'value' => $calendarId),
2979 $events = Calendar_Controller_Event::getInstance()->search($filter);
2980 Calendar_Model_Rrule::mergeAndRemoveNonMatchingRecurrences($events, $filter);
2985 * add calendar owner as attendee if not already set
2987 * @param string $calendarId
2988 * @param Tinebase_DateTime $from
2989 * @param Tinebase_DateTime $until
2990 * @param boolean $dry run
2992 * @return number of updated events
2994 public function repairAttendee($calendarId, $from, $until, $dry = false)
2996 $container = Tinebase_Container::getInstance()->getContainerById($calendarId);
2997 if ($container->type !== Tinebase_Model_Container::TYPE_PERSONAL) {
2998 throw new Calendar_Exception('Only allowed for personal containers!');
3000 if ($container->owner_id !== Tinebase_Core::getUser()->getId()) {
3001 throw new Calendar_Exception('Only allowed for own containers!');
3005 while ($from->isEarlier($until)) {
3006 $endWeek = $from->getClone()->addWeek(1);
3008 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
3009 . ' Repairing period ' . $from . ' - ' . $endWeek);
3012 // TODO we need to detect events with DECLINED/DELETED attendee
3013 $events = $this->_getEventsForPeriodAndCalendar($calendarId, $from, $endWeek);
3017 if (count($events) == 0) {
3018 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
3019 . ' No events found');
3023 foreach ($events as $event) {
3024 // add attendee if not already set
3025 if ($event->isRecurInstance()) {
3026 // TODO get base event
3027 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
3028 . ' Skip recur instance ' . $event->toShortString());
3032 $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
3033 if (! $ownAttender) {
3034 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
3035 . ' Add missing attender to event ' . $event->toShortString());
3037 $attender = new Calendar_Model_Attender(array(
3038 'user_type' => Calendar_Model_Attender::USERTYPE_USER,
3039 'user_id' => Tinebase_Core::getUser()->contact_id,
3040 'status' => Calendar_Model_Attender::STATUS_ACCEPTED
3042 $event->attendee->addRecord($attender);
3044 $this->update($event);
3051 return $updateCount;
3054 public function sendTentativeNotifications()
3056 $eventNotificationController = Calendar_Controller_EventNotifications::getInstance();
3057 $calConfig = Calendar_Config::getInstance();
3058 if (true !== $calConfig->{Calendar_Config::TENTATIVE_NOTIFICATIONS}
3059 ->{Calendar_Config::TENTATIVE_NOTIFICATIONS_ENABLED}) {
3063 $days = $calConfig->{Calendar_Config::TENTATIVE_NOTIFICATIONS}->{Calendar_Config::TENTATIVE_NOTIFICATIONS_DAYS};
3064 $additionalFilters = $calConfig->{Calendar_Config::TENTATIVE_NOTIFICATIONS}
3065 ->{Calendar_Config::TENTATIVE_NOTIFICATIONS_FILTER};
3068 array('field' => 'period', 'operator' => 'within', 'value' => array(
3069 'from' => Tinebase_DateTime::now(),
3070 'until' => Tinebase_DateTime::now()->addDay($days)
3072 array('field' => 'status', 'operator' => 'equals', 'value' => Calendar_Model_Event::STATUS_TENTATIVE)
3075 if (null !== $additionalFilters) {
3076 $filter = array_merge($filter, $additionalFilters);
3079 $filter = new Calendar_Model_EventFilter($filter);
3080 foreach ($this->search($filter) as $event) {
3081 $eventNotificationController->doSendNotifications($event, null, 'tentative');
3086 * returns active fixed calendars for users (combines config and preference)
3089 * @throws Tinebase_Exception_NotFound
3091 public function getFixedCalendarIds()
3093 $fixedCalendars = (array) Calendar_Config::getInstance()->get(Calendar_Config::FIXED_CALENDARS);
3095 // add fixed calendars from user preference
3096 $fixedCalendarsPref = Tinebase_Core::getPreference('Calendar')->getValue(Calendar_Preference::FIXED_CALENDARS);
3097 if (is_array($fixedCalendarsPref)) {
3098 foreach ($fixedCalendarsPref as $container) {
3099 if (isset($container['id'])) {
3100 $fixedCalendars[] = $container['id'];
3105 return $fixedCalendars;