- Support exceptions and iTip messages with thisansfuture range

- Store two exceptions for the same occurence if necessary (with differing range)
- Update attendee status from iTip REPLY to all exceptions stored for the event
- Correctly handle exceptions on the first instance (main event)
This commit is contained in:
Thomas Bruederli 2015-02-17 11:36:01 +01:00
parent 7fd2eb873d
commit 8a90069071
6 changed files with 239 additions and 61 deletions

View file

@ -863,6 +863,7 @@ class calendar extends rcube_plugin
$this->write_preprocess($event, $action);
if ($success = $this->driver->new_event($event)) {
$event['id'] = $event['uid'];
$event['_savemode'] = 'all';
$this->cleanup_event($event);
}
$reload = $success && $event['recurrence'] ? 2 : 1;
@ -1017,11 +1018,18 @@ class calendar extends rcube_plugin
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
$event['comment'] = $reply_comment;
$event['thisandfuture'] = $event['_savemode'] == 'future';
if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
// refresh all calendars
if ($event['calendar'] != $ev['calendar']) {
$this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true));
$reload = 0;
}
}
break;
@ -1139,11 +1147,13 @@ class calendar extends rcube_plugin
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
$event['_savemode'] = $_savemode;
// send notification for the main event when savemode is 'all'
if ($_savemode == 'all' && $event['recurrence_id']) {
$event['id'] = $event['recurrence_id'];
$event = $this->driver->get_event($event);
unset($event['_instance'], $event['recurrence_date']);
}
// only notify if data really changed (TODO: do diff check on client already)
@ -2763,9 +2773,11 @@ class calendar extends rcube_plugin
}
}
$event_attendee = null;
$update_attendees = array();
foreach ($event['attendees'] as $attendee) {
if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
$event_attendee = $attendee;
$update_attendees[] = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
@ -2775,9 +2787,12 @@ class calendar extends rcube_plugin
}
// also copy delegate attendee
else if (!empty($attendee['delegated-from']) &&
(stripos($attendee['delegated-from'], $event['_sender']) !== false || stripos($attendee['delegated-from'], $event['_sender_utf']) !== false) &&
(!in_array($attendee['email'], $existing_attendee_emails))) {
$existing['attendees'][] = $attendee;
(stripos($attendee['delegated-from'], $event['_sender']) !== false ||
stripos($attendee['delegated-from'], $event['_sender_utf']) !== false)) {
$update_attendees[] = $attendee;
if (!in_array($attendee['email'], $existing_attendee_emails)) {
$existing['attendees'][] = $attendee;
}
}
}
@ -2794,12 +2809,12 @@ class calendar extends rcube_plugin
// found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $event_attendee) {
$existing['attendees'][$existing_attendee] = $event_attendee;
$success = $this->driver->edit_event($existing);
$success = $this->driver->update_attendees($existing, $update_attendees);
}
// update the entire attendees block
else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
$existing['attendees'][] = $event_attendee;
$success = $this->driver->edit_event($existing);
$success = $this->driver->update_attendees($existing, $update_attendees);
}
else {
$error_msg = $this->gettext('newerversionexists');
@ -2855,7 +2870,7 @@ class calendar extends rcube_plugin
// if the RSVP reply only refers to a single instance:
// store unmodified master event with current instance as exception
if (!empty($instance) && $savemode != 'all') {
if (!empty($instance) && !empty($savemode) && $savemode != 'all') {
$master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event');
if ($master['recurrence'] && !$master['_instance']) {
// compute recurring events until this instance's date

View file

@ -204,6 +204,18 @@ abstract class calendar_driver
return $this->edit_event($event);
}
/**
* Update the participant status for the given attendee
*
* @param array Hash array with event properties
* @param array List of hash arrays each represeting an updated attendee
* @return boolean True on success, False on error
*/
public function update_attendees(&$event, $attendees)
{
return $this->edit_event($event);
}
/**
* Move a single event
*

View file

@ -195,7 +195,11 @@ class kolab_calendar extends kolab_storage_folder_api
if ($master_id != $id && ($record = $this->storage->get_object($master_id)))
$this->events[$master_id] = $this->_to_rcube_event($record);
if (($master = $this->events[$master_id]) && $master['recurrence']) {
// check for match on the first instance already
if (($_instance = $this->events[$master_id]['_instance']) && $id == $master_id . '-' . $_instance) {
$this->events[$id] = $this->events[$master_id];
}
else if (($master = $this->events[$master_id]) && $master['recurrence']) {
$this->get_recurring_events($record, $master['start'], null, $id);
}
}
@ -274,17 +278,10 @@ class kolab_calendar extends kolab_storage_folder_api
$add = true;
// skip the first instance of a recurring event if listed in exdate
if ($virtual && (!empty($event['recurrence']['EXDATE']) || !empty($event['recurrence']['EXCEPTIONS']))) {
if ($virtual && !empty($event['recurrence']['EXDATE'])) {
$event_date = $event['start']->format('Ymd');
$exdates = (array)$event['recurrence']['EXDATE'];
// add dates from exceptions to list
if (is_array($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
$exdates[] = clone $exception['start'];
}
}
foreach ($exdates as $exdate) {
if ($exdate->format('Ymd') == $event_date) {
$add = false;
@ -293,6 +290,18 @@ class kolab_calendar extends kolab_storage_folder_api
}
}
// find and merge exception for the first instance
if (!empty($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS'])) {
$event_date = $event['start']->format('Ymd');
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
$exdate = $exception['recurrence_date'] ? $exception['recurrence_date']->format('Ymd') : substr($exception['_instance'], 0, 8);
if ($exdate == $event_date) {
$event['_instance'] = $exception['_instance'];
kolab_driver::merge_event_data($event, $exception);
}
}
}
if ($add)
$events[] = $event;
}
@ -583,24 +592,29 @@ class kolab_calendar extends kolab_storage_folder_api
$rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
$rec_event['isexception'] = 1;
// found the specifically requested instance, exiting...
if ($rec_event['id'] == $event_id) {
// found the specifically requested instance: register exception (single occurrence wins)
if ($rec_event['id'] == $event_id && (!$this->events[$event_id] || $this->events[$event_id]['thisandfuture'])) {
$rec_event['recurrence'] = $recurrence_rule;
$rec_event['recurrence_id'] = $event['uid'];
$events[] = $rec_event;
$this->events[$rec_event['id']] = $rec_event;
return $events;
}
// remember this exception's date
$exdate = substr($exception['_instance'], 0, 8);
$exdata[$exdate] = $rec_event;
if (!$exdata[$exdate] || $exdata[$exdate]['thisandfuture']) {
$exdata[$exdate] = $rec_event;
}
if ($rec_event['thisandfuture']) {
$futuredata[$exdate] = $rec_event;
}
}
}
// found the specifically requested instance, exiting...
if ($event_id && !empty($this->events[$event_id])) {
return array($this->events[$event_id]);
}
// use libkolab to compute recurring events
if (class_exists('kolabcalendaring')) {
$recurrence = new kolab_date_recurrence($object);
@ -627,7 +641,7 @@ class kolab_calendar extends kolab_storage_folder_api
$rec_event['_instance'] = $instance_id;
if ($overlay_data || $exdata[$datestr]) // copy data from exception
$this->_merge_event_data($rec_event, $exdata[$datestr] ?: $overlay_data);
kolab_driver::merge_event_data($rec_event, $exdata[$datestr] ?: $overlay_data);
$rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['uid'];
@ -651,38 +665,11 @@ class kolab_calendar extends kolab_storage_folder_api
return $events;
}
/**
* Merge certain properties from the overlay event to the base event object
*
* @param array The event object to be altered
* @param array The overlay event object to be merged over $event
*/
private function _merge_event_data(&$event, $overlay)
{
static $forbidden = array('id','uid','recurrence','recurrence_date','organizer','_attachments');
foreach ($overlay as $prop => $value) {
// adjust time of the recurring event instance
if ($prop == 'start' || $prop == 'end') {
if (is_object($event[$prop]) && is_a($event[$prop], 'DateTime')) {
$event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
// set date value if overlay is an exception of the current instance
if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
$event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
}
}
}
else if ($prop[0] != '_' && !in_array($prop, $forbidden))
$event[$prop] = $value;
}
}
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_event($record)
{
$record['id'] = $record['uid'];
$record['calendar'] = $this->id;
$record['links'] = $this->get_links($record['uid']);

View file

@ -537,7 +537,7 @@ class kolab_driver extends calendar_driver
$id = $event['id'] ?: $event['uid'];
$cal = $event['calendar'];
// we're looking for a recurring instance: expand the ID to our internal convention for recurring instanced
// we're looking for a recurring instance: expand the ID to our internal convention for recurring instances
if (!$event['id'] && $event['_instance']) {
$id .= '-' . $event['_instance'];
}
@ -634,6 +634,48 @@ class kolab_driver extends calendar_driver
return $ret;
}
/**
* Update the participant status for the given attendees
*
* @see calendar_driver::update_attendees()
*/
public function update_attendees(&$event, $attendees)
{
// for this-and-future updates, merge the updated attendees onto all exceptions in range
if (($event['_savemode'] == 'future' && $event['recurrence_id']) || !empty($event['recurrence'])) {
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
// load master event
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
// apply attendee update to each existing exception
if ($master['recurrence'] && !empty($master['recurrence']['EXCEPTIONS'])) {
$saved = false;
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
// merge the new event properties onto future exceptions
if ($exception['_instance'] >= $event['_instance']) {
self::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees);
}
// update a specific instance
if ($exception['_instance'] == $event['_instance'] && $exception['thisandfuture']) {
$saved = true;
}
}
// add the given event as new exception
if (!$saved && $event['id'] != $master['id']) {
$event['thisandfuture'] = true;
$master['recurrence']['EXCEPTIONS'][] = $event;
}
return $this->update_event($master);
}
}
// just update the given event (instance)
return $this->update_event($event);
}
/**
* Move a single event
@ -933,15 +975,10 @@ class kolab_driver extends calendar_driver
}
// save properties to a recurrence exception instance
if ($old['recurrence_id'] && is_array($master['recurrence']['EXCEPTIONS'])) {
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
if ($exception['_instance'] == $old['_instance']) {
$event['_instance'] = $old['_instance'];
$event['recurrence_date'] = $old['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$success = $storage->update_event($master, $old['id']);
break 2;
}
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;
}
}
@ -962,7 +999,7 @@ class kolab_driver extends calendar_driver
// save as new exception to master event
if ($add_exception) {
$event['_instance'] = $old['_instance'];
$event['recurrence_date'] = $old['recurrence_date'];
$event['recurrence_date'] = $old['recurrence_date'] ?: $old['start'];
$master['recurrence']['EXCEPTIONS'][] = $event;
}
$success = $storage->update_event($master);
@ -1004,6 +1041,8 @@ class kolab_driver extends calendar_driver
$event['end'] = $master['end'];
}
// TODO: forward changes to exceptions (which do not yet have differing values stored)
// 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) &&
is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
@ -1070,6 +1109,120 @@ class kolab_driver extends calendar_driver
return $reschedule;
}
/**
* Apply the given changes to already existing exceptions
*/
protected function update_recurrence_exceptions(&$master, $event, $old, $savemode)
{
$saved = false;
$existing = null;
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
// update a specific instance
if ($exception['_instance'] == $old['_instance']) {
$existing = $i;
// check savemode against existing exception mode.
// if matches, we can update this existing exception
if ((bool)$exception['thisandfuture'] === ($savemode == 'future')) {
$event['_instance'] = $old['_instance'];
$event['thisandfuture'] = $old['thisandfuture'];
$event['recurrence_date'] = $old['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$saved = true;
}
}
// merge the new event properties onto future exceptions
if ($savemode == 'future' && $exception['_instance'] >= $old['_instance']) {
unset($event['thisandfuture']);
self::merge_event_data($master['recurrence']['EXCEPTIONS'][$i], $event);
}
}
/*
// we could not update the existing exception due to savemode mismatch...
if (!$saved && $existing !== null && $master['recurrence']['EXCEPTIONS'][$existing]['thisandfuture']) {
// ... try to move the existing this-and-future exception to the next occurrence
foreach ($this->get_recurring_events($master, $existing['start']) as $candidate) {
// our old this-and-future exception is obsolete
if ($candidate['thisandfuture']) {
unset($master['recurrence']['EXCEPTIONS'][$existing]);
$saved = true;
break;
}
// this occurrence doesn't yet have an exception
else if (!$candidate['isexception']) {
$event['_instance'] = $candidate['_instance'];
$event['recurrence_date'] = $candidate['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$saved = true;
break;
}
}
}
*/
// returning false here will add a new exception
return $saved;
}
/**
* Merge certain properties from the overlay event to the base event object
*
* @param array The event object to be altered
* @param array The overlay event object to be merged over $event
*/
public static function merge_event_data(&$event, $overlay)
{
static $forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
foreach ($overlay as $prop => $value) {
// adjust time of the recurring event instance
if ($prop == 'start' || $prop == 'end') {
if (is_object($event[$prop]) && is_a($event[$prop], 'DateTime')) {
$event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
// set date value if overlay is an exception of the current instance
if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
$event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
}
}
}
else if ($prop == 'thisandfuture' && $overlay['_instance'] == $event['_instance']) {
$event[$prop] = $value;
}
else if ($prop[0] != '_' && !in_array($prop, $forbidden))
$event[$prop] = $value;
}
}
/**
* Update attendee properties on the given event object
*
* @param array The event object to be altered
* @param array List of hash arrays each represeting an updated/added attendee
*/
public static function merge_attendee_data(&$event, $attendees)
{
if (!empty($attendees) && !is_array($attendees[0])) {
$attendees = array($attendees);
}
foreach ($attendees as $attendee) {
$found = false;
foreach ($event['attendees'] as $i => $candidate) {
if ($candidate['email'] == $attendee['email']) {
$event['attendees'][$i] = $attendee;
$found = true;
break;
}
}
if (!$found) {
$event['attendees'][] = $attendee;
}
}
}
/**
* Get events from source.
*
@ -1520,6 +1673,13 @@ class kolab_driver extends calendar_driver
if (empty($record['recurrence']))
unset($record['recurrence']);
// add instance identifier to first occurrence (master event)
if ($record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) {
$recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
$record['recurrence_date'] = $record['start'];
$record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
}
// remove internals
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);

View file

@ -1410,8 +1410,12 @@ class libcalendaring extends rcube_plugin
*/
public static function identify_recurrence_instance(&$object)
{
// for savemode=all, remove recurrence instance identifiers
if (!empty($object['_savemode']) && $object['_savemode'] == 'all') {
unset($object['_instance'], $object['recurrence_date']);
}
// set instance and 'savemode' according to recurrence-id
if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
else if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
$recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
$object['_instance'] = $object['recurrence_date']->format($recurrence_id_format);
$object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current';

View file

@ -109,7 +109,7 @@ $labels['acceptattendee'] = 'Accept participant';
$labels['declineattendee'] = 'Decline participant';
$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
$labels['rsvpmodeall'] = 'The entire series';
$labels['rsvpmodecurrent'] = 'This occurrence';
$labels['rsvpmodecurrent'] = 'This occurrence only';
$labels['rsvpmodefuture'] = 'This and future occurrences';
$labels['itipsingleoccurrence'] = 'This is a <em>single occurrence</em> out of a series of events';