/** * Wrapper to update an event object depending on the given savemode */ private function update_event($event) { if (!($storage = $this->get_calendar($event['calendar']))) { return false; } // move event to another folder/calendar if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) { if (!($fromcalendar = $this->get_calendar($event['_fromcalendar']))) { return false; } $old = $fromcalendar->get_event($event['id']); if ($event['_savemode'] != 'new') { if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) { return false; } $fromcalendar = $storage; } } else { $fromcalendar = $storage; } $success = false; $savemode = 'all'; $attachments = array(); $old = $master = $storage->get_event($event['id']); if (!$old || !$old['start']) { rcube::raise_error(array('code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to load event object to update: id=" . $event['id']), true, false); return false; } // modify a recurring event, check submitted savemode to do the right things if ($old['recurrence'] || $old['recurrence_id'] || $old['isexception']) { $master = $storage->get_event($old['uid']); $savemode = $event['_savemode'] ?: ($old['recurrence_id'] || $old['isexception'] ? 'current' : 'all'); // this-and-future on the first instance equals to 'all' if ($savemode == 'future' && $master['start'] && $old['_instance'] == libcalendaring::recurrence_instance_identifier($master)) { $savemode = 'all'; } else { if (!$old['recurrence'] && !$old['recurrence_id'] && $old['isexception']) { $savemode = 'current'; } } } // check if update affects scheduling and update attendee status accordingly $reschedule = $this->check_scheduling($event, $old, true); // keep saved exceptions (not submitted by the client) if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE'])) { $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE']; } if (isset($event['recurrence']['EXCEPTIONS'])) { $with_exceptions = true; } else { if ($old['recurrence']['EXCEPTIONS']) { $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS']; } else { if ($old['exceptions']) { $event['exceptions'] = $old['exceptions']; } } } // remove some internal properties which should not be saved unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'], $event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']); switch ($savemode) { case 'new': // save submitted data as new (non-recurring) event $event['recurrence'] = array(); $event['_copyfrom'] = $master['_msguid']; $event['_mailbox'] = $master['_mailbox']; $event['uid'] = $this->cal->generate_uid(); unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']); // copy attachment metadata to new event $event = self::from_rcube_event($event, $master); self::clear_attandee_noreply($event); if ($success = $storage->insert_event($event)) { $success = $event['uid']; } break; case 'future': // create a new recurring event $event['_copyfrom'] = $master['_msguid']; $event['_mailbox'] = $master['_mailbox']; $event['uid'] = $this->cal->generate_uid(); unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']); // copy attachment metadata to new event $event = self::from_rcube_event($event, $master); // remove recurrence exceptions on re-scheduling if ($reschedule) { unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']); } else { if (is_array($event['recurrence']['EXCEPTIONS'])) { // only keep relevant exceptions $event['recurrence']['EXCEPTIONS'] = array_filter($event['recurrence']['EXCEPTIONS'], function ($exception) use($event) { return $exception['start'] > $event['start']; }); if (is_array($event['recurrence']['EXDATE'])) { $event['recurrence']['EXDATE'] = array_filter($event['recurrence']['EXDATE'], function ($exdate) use($event) { return $exdate > $event['start']; }); } // set link to top-level exceptions $event['exceptions'] =& $event['recurrence']['EXCEPTIONS']; } } // compute remaining occurrences if ($event['recurrence']['COUNT']) { if (!$old['_count']) { $old['_count'] = $this->get_recurrence_count($master, $old['start']); } $event['recurrence']['COUNT'] -= intval($old['_count']); } // remove fixed weekday when date changed if ($old['start']->format('Y-m-d') != $event['start']->format('Y-m-d')) { if (strlen($event['recurrence']['BYDAY']) == 2) { unset($event['recurrence']['BYDAY']); } if ($old['recurrence']['BYMONTH'] == $old['start']->format('n')) { unset($event['recurrence']['BYMONTH']); } } // set until-date on master event $master['recurrence']['UNTIL'] = clone $old['start']; $master['recurrence']['UNTIL']->sub(new DateInterval('P1D')); unset($master['recurrence']['COUNT']); // remove all exceptions after $event['start'] if (is_array($master['recurrence']['EXCEPTIONS'])) { $master['recurrence']['EXCEPTIONS'] = array_filter($master['recurrence']['EXCEPTIONS'], function ($exception) use($event) { return $exception['start'] < $event['start']; }); // set link to top-level exceptions $master['exceptions'] =& $master['recurrence']['EXCEPTIONS']; } if (is_array($master['recurrence']['EXDATE'])) { $master['recurrence']['EXDATE'] = array_filter($master['recurrence']['EXDATE'], function ($exdate) use($event) { return $exdate < $event['start']; }); } // save new event if ($success = $storage->insert_event($event)) { $success = $event['uid']; // update master event (no rescheduling!) self::clear_attandee_noreply($master); $storage->update_event($master); } break; case 'current': // recurring instances shall not store recurrence rules and attachments $event['recurrence'] = array(); $event['thisandfuture'] = $savemode == 'future'; unset($event['attachments'], $event['id']); // increment sequence of this instance if scheduling is affected if ($reschedule) { $event['sequence'] = max($old['sequence'], $master['sequence']) + 1; } else { if (!isset($event['sequence'])) { $event['sequence'] = $old['sequence'] ?: $master['sequence']; } } // save properties to a recurrence exception instance if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) { if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) { $success = $storage->update_event($master, $old['id']); break; } } $add_exception = true; // adjust matching RDATE entry if dates changed if (is_array($master['recurrence']['RDATE']) && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) { foreach ($master['recurrence']['RDATE'] as $j => $rdate) { if ($rdate->format('Ymd') == $old_date) { $master['recurrence']['RDATE'][$j] = $event['start']; sort($master['recurrence']['RDATE']); $add_exception = false; break; } } } // save as new exception to master event if ($add_exception) { self::add_exception($master, $event, $old); } $success = $storage->update_event($master); break; default: // 'all' is default $event['id'] = $master['uid']; $event['uid'] = $master['uid']; // use start date from master but try to be smart on time or duration changes $old_start_date = $old['start']->format('Y-m-d'); $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i'); $old_duration = $old['end']->format('U') - $old['start']->format('U'); $new_start_date = $event['start']->format('Y-m-d'); $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i'); $new_duration = $event['end']->format('U') - $event['start']->format('U'); $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration; $date_shift = $old['start']->diff($event['start']); // shifted or resized if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) { $event['start'] = $master['start']->add($date_shift); $event['end'] = clone $event['start']; $event['end']->add(new DateInterval('PT' . $new_duration . 'S')); // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event() if ($old_start_date != $new_start_date) { if (strlen($event['recurrence']['BYDAY']) == 2) { unset($event['recurrence']['BYDAY']); } if ($old['recurrence']['BYMONTH'] == $old['start']->format('n')) { unset($event['recurrence']['BYMONTH']); } } } else { if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) { $event['start'] = $master['start']; $event['end'] = $master['end']; } } // when saving an instance in 'all' mode, copy recurrence exceptions over if ($old['recurrence_id']) { $event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS']; } else { if ($master['_instance']) { $event['_instance'] = $master['_instance']; $event['recurrence_date'] = $master['recurrence_date']; } } // TODO: forward changes to exceptions (which do not yet have differing values stored) if (is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) { // determine added and removed attendees $old_attendees = $current_attendees = $added_attendees = array(); foreach ((array) $old['attendees'] as $attendee) { $old_attendees[] = $attendee['email']; } foreach ((array) $event['attendees'] as $attendee) { $current_attendees[] = $attendee['email']; if (!in_array($attendee['email'], $old_attendees)) { $added_attendees[] = $attendee; } } $removed_attendees = array_diff($old_attendees, $current_attendees); foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) { calendar::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees); } // adjust recurrence-id when start changed and therefore the entire recurrence chain changes if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) { $recurrence_id_format = libcalendaring::recurrence_id_format($event); foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) { $recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] : rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone()); if (is_a($recurrence_id, 'DateTime')) { $recurrence_id->add($date_shift); $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id; $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format); } } } // set link to top-level exceptions $event['exceptions'] =& $event['recurrence']['EXCEPTIONS']; } // unset _dateonly flags in (cached) date objects unset($event['start']->_dateonly, $event['end']->_dateonly); $success = $storage->update_event($event) ? $event['id'] : false; // return master UID break; } if ($success && $this->freebusy_trigger) { $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); } return $success; }
/** * Create instances of a recurring event * * @param array Hash array with event properties * @param object DateTime Start date of the recurrence window * @param object DateTime End date of the recurrence window * @return array List of recurring event instances */ public function get_recurring_events($event, $start, $end = null) { $events = array(); if ($event['recurrence']) { // include library class require_once dirname(__FILE__) . '/../lib/calendar_recurrence.php'; $rcmail = rcmail::get_instance(); $recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event); $recurrence_id_format = libcalendaring::recurrence_id_format($event); // determine a reasonable end date if none given if (!$end) { switch ($event['recurrence']['FREQ']) { case 'YEARLY': $intvl = 'P100Y'; break; case 'MONTHLY': $intvl = 'P20Y'; break; default: $intvl = 'P10Y'; break; } $end = clone $event['start']; $end->add(new DateInterval($intvl)); } $i = 0; while ($next_event = $recurrence->next_instance()) { // add to output if in range if ($next_event['start'] <= $end && $next_event['end'] >= $start) { $next_event['_instance'] = $next_event['start']->format($recurrence_id_format); $next_event['id'] = $next_event['uid'] . '-' . $exception['_instance']; $next_event['recurrence_id'] = $event['uid']; $events[] = $next_event; } else { if ($next_event['start'] > $end) { // stop loop if out of range break; } } // avoid endless recursion loops if (++$i > 1000) { break; } } } return $events; }
/** * Insert "fake" entries for recurring occurences of this event */ private function _update_recurring($event) { if (empty($this->calendars)) { return; } if (!empty($event['recurrence'])) { $exdata = array(); $exceptions = $this->_load_exceptions($event); foreach ($exceptions as $exception) { $exdate = substr($exception['_instance'], 0, 8); $exdata[$exdate] = $exception; } } // clear existing recurrence copies $this->rc->db->query("DELETE FROM " . $this->db_events . "\n WHERE recurrence_id=?\n AND isexception=0\n AND calendar_id IN (" . $this->calendar_ids . ")", $event['id']); // create new fake entries if (!empty($event['recurrence'])) { // include library class require_once $this->cal->home . '/lib/calendar_recurrence.php'; $recurrence = new calendar_recurrence($this->cal, $event); $count = 0; $event['allday'] = $event['all_day']; $duration = $event['start']->diff($event['end']); $recurrence_id_format = libcalendaring::recurrence_id_format($event); while ($next_start = $recurrence->next_start()) { $instance = $next_start->format($recurrence_id_format); $datestr = substr($instance, 0, 8); // skip exceptions // TODO: merge updated data from master event if ($exdata[$datestr]) { continue; } $next_start->setTimezone($this->server_timezone); $next_end = clone $next_start; $next_end->add($duration); $notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_start, 'end' => $next_end, 'status' => $event['status'])); $query = $this->rc->db->query(sprintf("INSERT INTO " . $this->db_events . "\n (calendar_id, recurrence_id, created, changed, uid, instance, %s, %s, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, notifyat)\n SELECT calendar_id, ?, %s, %s, uid, ?, ?, ?, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, ?\n FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")", $this->rc->db->quote_identifier('start'), $this->rc->db->quote_identifier('end'), $this->rc->db->now(), $this->rc->db->now()), $event['id'], $instance, $next_start->format(self::DB_DATE_FORMAT), $next_end->format(self::DB_DATE_FORMAT), $notify_at, $event['id']); if (!$this->rc->db->affected_rows($query)) { break; } // stop adding events for inifinite recurrence after 20 years if (++$count > 999 || !$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20) { break; } } // remove all exceptions after recurrence end if ($next_end && !empty($exceptions)) { $this->rc->db->query("DELETE FROM " . $this->db_events . "\n WHERE `recurrence_id`=?\n AND `isexception`=1\n AND `start` > ?\n AND `calendar_id` IN (" . $this->calendar_ids . ")", $event['id'], $next_end->format(self::DB_DATE_FORMAT)); } } }
/** * Convert from Kolab_Format to internal representation */ private function _to_driver_event($record, $noinst = false) { $record['calendar'] = $this->id; $record['links'] = $this->get_links($record['uid']); if ($this->get_namespace() == 'other') { $record['className'] = 'fc-event-ns-other'; $record = kolab_driver::add_partstat_class($record, array('NEEDS-ACTION', 'DECLINED'), $this->get_owner()); } // add instance identifier to first occurrence (master event) $recurrence_id_format = libcalendaring::recurrence_id_format($record); if (!$noinst && $record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) { $record['_instance'] = $record['start']->format($recurrence_id_format); } else { if (is_a($record['recurrence_date'], 'DateTime')) { $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format); } } // clean up exception data if ($record['recurrence'] && is_array($record['recurrence']['EXCEPTIONS'])) { array_walk($record['recurrence']['EXCEPTIONS'], function (&$exception) { unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']); }); } return $record; }