From 8a90069071821f4cf2cdac6d7fce82cf12a32c11 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Tue, 17 Feb 2015 11:36:01 +0100 Subject: [PATCH] - 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) --- plugins/calendar/calendar.php | 27 ++- plugins/calendar/drivers/calendar_driver.php | 12 ++ .../calendar/drivers/kolab/kolab_calendar.php | 71 +++---- .../calendar/drivers/kolab/kolab_driver.php | 182 ++++++++++++++++-- plugins/libcalendaring/libcalendaring.php | 6 +- plugins/libcalendaring/localization/en_US.inc | 2 +- 6 files changed, 239 insertions(+), 61 deletions(-) diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 786cd768..8e6a1a3c 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -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 diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index 742830b6..aecd2e17 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -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 * diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index b5b272fd..29bc01e5 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -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']); diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 0cd962df..3e3f0fcb 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -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']); diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php index 63a05488..c2038548 100644 --- a/plugins/libcalendaring/libcalendaring.php +++ b/plugins/libcalendaring/libcalendaring.php @@ -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'; diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc index ca7d1fd5..e5e04261 100644 --- a/plugins/libcalendaring/localization/en_US.inc +++ b/plugins/libcalendaring/localization/en_US.inc @@ -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 single occurrence out of a series of events';