Reliably identify recurrence instances throughout the application to support invitations of recurring events (#4387)

This commit is contained in:
Thomas Bruederli 2015-02-15 14:32:31 +01:00
parent a6ba6981e9
commit 78622133a9
14 changed files with 189 additions and 49 deletions

View file

@ -841,15 +841,12 @@ class calendar extends rcube_plugin
$event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true);
$success = $reload = $got_msg = false;
// don't notify if modifying a recurring instance (really?)
if ($event['_savemode'] && in_array($event['_savemode'], array('current','future')) && $event['_notify'] && $action != 'remove')
unset($event['_notify']);
// force notify if hidden + active
else if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1)
if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1)
$event['_notify'] = 1;
// read old event data in order to find changes
if (($event['_notify'] || $event['decline']) && $action != 'new')
if (($event['_notify'] || $event['_decline']) && $action != 'new')
$old = $this->driver->get_event($event);
switch ($action) {
@ -928,8 +925,14 @@ class calendar extends rcube_plugin
$got_msg = true;
}
// send cancellation for the main event
if ($event['_savemode'] == 'all')
unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']);
else if ($event['_savemode'] == 'future')
$old['thisandfuture'] = true;
// send iTIP reply that participant has declined the event
if ($success && $event['decline']) {
if ($success && $event['_decline']) {
$emails = $this->get_user_emails();
foreach ($old['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
@ -939,7 +942,7 @@ class calendar extends rcube_plugin
$reply_sender = $attendee['email'];
}
}
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
@ -974,6 +977,7 @@ class calendar extends rcube_plugin
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$ev['free_busy'] = $event['free_busy'];
$ev['_savemode'] = $event['_savemode'];
// send invitation to delegatee + add it as attendee
if ($status == 'delegated' && $event['to']) {
@ -1127,11 +1131,7 @@ class calendar extends rcube_plugin
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
// sending notification on a recurrence instance -> re-send the main event
if ($event['recurrence_id']) {
$event = $this->driver->get_event(array('id' => $event['recurrence_id'], 'cal' => $event['calendar']));
$action = 'edit';
}
// TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
// only notify if data really changed (TODO: do diff check on client already)
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
@ -1945,6 +1945,9 @@ class calendar extends rcube_plugin
// add comment to the iTip attachment
$event['comment'] = $comment;
// set a valid recurrence-id if this is a recurrence instance
libcalendaring::identify_recurrence_instance($event);
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
$message = $itip->compose_itip_message($event, $method, $event['sequence'] > $old['sequence']);
@ -2405,9 +2408,10 @@ class calendar extends rcube_plugin
{
$success = false;
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
$inst = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
// search for event if only UID is given
if ($event = $this->driver->get_event(array('uid' => $uid), true)) {
if ($event = $this->driver->get_event(array('uid' => $uid, '_instance' => $inst), true)) {
$success = $this->driver->remove_event($event, true);
}
@ -2722,12 +2726,13 @@ class calendar extends rcube_plugin
// save to calendar
if ($calendar && !$calendar['readonly']) {
$event['calendar'] = $calendar['id'];
// check for existing event with the same UID
$existing = $this->driver->get_event($event['uid'], true, false, true);
// check for existing event with the same UID
$existing = $this->driver->get_event($event, true, false, true);
if ($existing) {
// forward savemode for correct updates of recurring events
$existing['_savemode'] = $event['_savemode'];
// only update attendee status
if ($event['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address
@ -2829,6 +2834,8 @@ class calendar extends rcube_plugin
if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
$event['free_busy'] = 'free';
}
// save to the selected/default calendar
$event['calendar'] = $calendar['id'];
$success = $this->driver->new_event($event);
}
else if ($status == 'declined')

View file

@ -554,6 +554,14 @@ function rcube_calendar_ui(settings)
$('#event-rsvp a.reply-comment-toggle').show();
$('#event-rsvp .itip-reply-comment textarea').hide().val('');
if (event.recurrence && event.id) {
var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
$('#event-rsvp input.rsvp-replymode[value="'+sel+'"]').prop('checked', true);
$('#event-rsvp .rsvp-replymode-message').show();
}
else
$('#event-rsvp .rsvp-replymode-message').hide();
}
var buttons = [];
@ -742,11 +750,9 @@ function rcube_calendar_ui(settings)
// show warning if editing a recurring event
if (event.id && event.recurrence) {
var allow_exceptions = !has_attendees(event) || !is_organizer(event),
sel = event._savemode || (allow_exceptions && event.thisandfuture ? 'future' : (allow_exceptions && event.isexception ? 'current' : 'all'));
var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
$('#edit-recurring-warning').show();
$('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true);
$('input.edit-recurring-savemode[value="current"], input.edit-recurring-savemode[value="future"]').prop('disabled', !allow_exceptions);
}
else
$('#edit-recurring-warning').hide();
@ -2411,7 +2417,7 @@ function rcube_calendar_ui(settings)
}
// submit status change to server
var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val() }, (delegate || {})),
var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val(), _savemode: $('input.rsvp-replymode:checked').val() }, (delegate || {})),
noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
// import event from mail (temporary iTip event)
@ -2425,7 +2431,8 @@ function rcube_calendar_ui(settings)
_to: (delegate ? delegate.to : null),
_rsvp: (delegate && delegate.rsvp) ? 1 : 0,
_noreply: noreply,
_comment: submit_data.comment
_comment: submit_data.comment,
_savemode: submit_data._savemode
});
}
else if (settings.invitation_calendars) {
@ -2501,7 +2508,7 @@ function rcube_calendar_ui(settings)
// mark all recurring instances as temp
if (event.recurrence || event.recurrence_id) {
var base_id = event.recurrence_id ? event.recurrence_id.replace(/-\d+$/, '') : event.id;
var base_id = event.recurrence_id ? event.recurrence_id.replace(/-\d+(T\d{6})?$/, '') : event.id;
$.each(fc.fullCalendar('clientEvents', function(e){ return e.id == base_id || e.recurrence_id == base_id; }), function(i,ev) {
ev.temp = true;
ev.editable = false;
@ -2566,7 +2573,7 @@ function rcube_calendar_ui(settings)
// recurring event: user needs to select the savemode
if (event.recurrence) {
var disabled_state = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
/*
if (_has_attendees) {
if (action == 'remove') {
if (!_is_organizer) {
@ -2578,7 +2585,7 @@ function rcube_calendar_ui(settings)
disabled_state = ' disabled';
}
}
*/
html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
rcmail.gettext(message_label, 'calendar') + '</div>' +
'<div class="savemode">' +
@ -2606,8 +2613,10 @@ function rcube_calendar_ui(settings)
else {
if ($dialog.find('input.confirm-attendees-donotify').length)
data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
if (decline && $dialog.find('input.confirm-attendees-decline:checked').length)
data.decline = 1;
if (decline) {
data._decline = $dialog.find('input.confirm-attendees-decline:checked').length;
data._notify = 0;
}
update_event(action, data);
}
@ -2622,7 +2631,7 @@ function rcube_calendar_ui(settings)
text: rcmail.gettext((action == 'remove' ? 'delete' : 'save'), 'calendar'),
click: function() {
data._notify = notify && $dialog.find('input.confirm-attendees-donotify:checked').length ? 1 : 0;
data.decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
data._decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
update_event(action, data);
$(this).dialog("close");
}

View file

@ -50,6 +50,7 @@
* 'EXCEPTIONS' => array(<event>), list of event objects which denote exceptions in the recurrence chain
* ),
* 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event
* '_instance' => 'ID of the recurring instance', // identifies an instance within a recurrence chain
* 'categories' => 'Event category',
* 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as
* 'status' => 'TENTATIVE|CONFIRMED|CANCELLED', // event status according to RFC 2445
@ -469,7 +470,6 @@ abstract class calendar_driver
if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
$next_event['id'] = $next_event['uid'];
$next_event['recurrence_id'] = $event['uid'];
$next_event['_instance'] = $i;
$events[] = $next_event;
}
else if ($next_event['start'] > $end) { // stop loop if out of range

View file

@ -661,7 +661,7 @@ class kolab_calendar extends kolab_storage_folder_api
*/
private function _merge_event_data(&$event, $overlay)
{
static $forbidden = array('id','uid','created','changed','recurrence','organizer','attendees','sequence');
static $forbidden = array('id','uid','recurrence','organizer','_attachments');
foreach ($overlay as $prop => $value) {
// adjust time of the recurring event instance

View file

@ -534,8 +534,13 @@ class kolab_driver extends calendar_driver
public function get_event($event, $writeable = false, $active = false, $personal = false)
{
if (is_array($event)) {
$id = $event['id'] ? $event['id'] : $event['uid'];
$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
if (!$event['id'] && $event['_instance']) {
$id .= '-' . $event['_instance'];
}
}
else {
$id = $event;
@ -687,11 +692,11 @@ class kolab_driver extends calendar_driver
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id']) {
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
$savemode = $event['_savemode'];
$savemode = $event['_savemode'] ?: ($event['_instance'] ? 'current' : 'all');
}
// removing an exception instance
if ($event['recurrence_id']) {
if ($event['recurrence_id'] && $master['recurrence'] && is_array($master['recurrence']['EXCEPTIONS'])) {
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
if ($exception['_instance'] == $event['_instance']) {
unset($master['recurrence']['EXCEPTIONS'][$i]);
@ -880,7 +885,7 @@ class kolab_driver extends calendar_driver
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id']) {
$master = $old['recurrence_id'] ? $fromcalendar->get_event($old['recurrence_id']) : $old;
$savemode = $event['_savemode'];
$savemode = $event['_savemode'] ?: ($old['recurrence_id'] ? 'current' : 'all');
}
// check if update affects scheduling and update attendee status accordingly
@ -919,7 +924,7 @@ class kolab_driver extends calendar_driver
// increment sequence of this instance if scheduling is affected
if ($reschedule) {
$event['sequence'] = $old['sequence'] + 1;
$event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
}
// remove some internal properties which should not be saved

View file

@ -67,7 +67,6 @@ class calendar_recurrence extends libcalendaring_recurrence
{
if ($next_start = $this->next()) {
$next = $this->event;
$next['recurrence_id'] = $next_start->format('Y-m-d');
$next['start'] = $next_start;
if ($this->duration) {
@ -75,6 +74,10 @@ class calendar_recurrence extends libcalendaring_recurrence
$next['end']->add($this->duration);
}
$recurrence_id_format = $next['allday'] ? 'Ymd' : 'Ymd\THis';
$next['recurrence_date'] = clone $next_start;
$next['_instance'] = $next_start->format($recurrence_id_format);
unset($next['_formatobj']);
return $next;

View file

@ -1059,6 +1059,26 @@ td.topalign {
text-align: center;
}
.event-dialog-message .rsvp-replymode-message {
margin-top: 0.8em;
margin-bottom: 0.6em;
}
.event-dialog-message .rsvp-replymode-message .replymode-select {
padding-left: 22px;
}
.event-dialog-message .rsvp-replymode-message label {
color: inherit;
margin-right: 0.4em;
white-space: nowrap;
min-width: 4em;
}
.event-dialog-message .rsvp-replymode-message input.rsvp-replymode {
margin-right: 0.4em;
}
#event-rsvp,
#edit-attendees-notify {
margin: 0.6em 0 0.3em 0;
@ -2159,6 +2179,15 @@ div.calendar-invitebox td.sensitivity {
font-weight: bold;
}
div.calendar-invitebox td.recurrence-id {
text-transform: uppercase;
font-style: italic;
}
div.calendar-invitebox td em {
font-weight: bold;
}
#event-rsvp .rsvp-buttons,
div.calendar-invitebox .itip-buttons div {
margin-top: 0.5em;

View file

@ -98,8 +98,10 @@ class libcalendaring_itip
if (!$this->sender['name'])
$this->sender['name'] = $this->sender['email'];
if (!$message)
if (!$message) {
libcalendaring::identify_recurrence_instance($event);
$message = $this->compose_itip_message($event, $method, $rsvp);
}
$mailto = rcube_idn_to_ascii($recipient['email']);
@ -121,12 +123,19 @@ class libcalendaring_itip
($attendee['name'] ? $attendee['name'] : $attendee['email']);
}
$recurrence_info = '';
if (!empty($event['recurrence_id'])) {
$recurrence_info = "\n\n** " . $this->gettext('itip'.strtolower($method).'occurrenceonly') . ' **';
}
else if (!empty($event['recurrence'])) {
$recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']));
}
$mailbody = $this->gettext(array(
'name' => $bodytext,
'vars' => array(
'title' => $event['title'],
'date' => $this->lib->event_date_text($event, true) .
(empty($event['recurrence']) ? '' : sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']))),
'date' => $this->lib->event_date_text($event, true) . $recurrence_info,
'attendees' => join(",\n ", $attendees_list),
'sender' => $this->sender['name'],
'organizer' => $this->sender['name'],
@ -151,6 +160,10 @@ class libcalendaring_itip
$message->headers($headers, true);
$message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
if ($this->rc->config->get('libcalendaring_itip_debug', false)) {
console('iTip ' . $method, $message->txtHeaders() . "\n\r" . $message->get());
}
// finally send the message
$this->itip_send = true;
$sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
@ -230,6 +243,9 @@ class libcalendaring_itip
array_unshift($reply_attendees, $replying_attendee);
$event['attendees'] = $reply_attendees;
}
if ($event['recurrence']) {
unset($event['recurrence']['EXCEPTIONS']);
}
}
// set RSVP for every attendee
else if ($method == 'REQUEST') {
@ -239,6 +255,11 @@ class libcalendaring_itip
}
}
}
else if ($method == 'CANCEL') {
if ($event['recurrence']) {
unset($event['recurrence']['EXCEPTIONS']);
}
}
// compose multipart message using PEAR:Mail_Mime
$message = new Mail_mime("\r\n");
@ -453,6 +474,7 @@ class libcalendaring_itip
$changed = is_object($event['changed']) ? $event['changed'] : $message_date;
$metadata = array(
'uid' => $event['uid'],
'_instance' => $event['_instance'],
'changed' => $changed ? $changed->format('U') : 0,
'sequence' => intval($event['sequence']),
'method' => $method,
@ -580,12 +602,13 @@ class libcalendaring_itip
// for CANCEL messages, we can:
else if ($method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
$event_prop = array_filter(array('uid' => $event['uid'], '_instance' => $event['_instance']));
// 1. remove the event from our calendar
$button_remove = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_libcalendaring.remove_from_itip('" . JQ($event['uid']) . "', '$task', '" . JQ($event['title']) . "')",
'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . JQ($event['title']) . "')",
'value' => $this->gettext('removefromcalendar'),
));
@ -646,8 +669,6 @@ class libcalendaring_itip
));
}
$buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']));
// add localized texts for the delegation dialog
if (in_array('delegated', $actions)) {
foreach (array('itipdelegated','itipcomment','delegateinvitation',
@ -656,9 +677,23 @@ class libcalendaring_itip
}
}
$savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode'));
return html::div($attrib,
html::div('label', $this->gettext('acceptinvitation')) .
html::div('rsvp-buttons', $buttons));
html::div('rsvp-buttons',
$buttons .
html::div(array('class' => 'rsvp-replymode-message', 'style' => 'display:none'),
html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->gettext('rsvprecurringevent')) .
html::div('replymode-select',
html::label(null, $savemode_radio->show('all', array('value' => 'all')) . $this->gettext('allevents')) .
html::label(null, $savemode_radio->show(null, array('value' => 'current')) . $this->gettext('currentevent')) .
html::label(null, $savemode_radio->show(null, array('value' => 'future')) . $this->gettext('futurevents'))
)
) .
html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
)
);
}
/**
@ -705,7 +740,11 @@ class libcalendaring_itip
$table->add('label', $this->gettext('date'));
$table->add('date', Q($this->lib->event_date_text($event)));
}
if (!empty($event['recurrence'])) {
if (!empty($event['recurrence_date'])) {
$table->add('label', '');
$table->add('recurrence-id', $this->gettext('itipsingleoccurrence'));
}
else if (!empty($event['recurrence'])) {
$table->add('label', $this->gettext('recurring'));
$table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
}

View file

@ -961,11 +961,11 @@ rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
/**
*
*/
rcube_libcalendaring.remove_from_itip = function(uid, task, title)
rcube_libcalendaring.remove_from_itip = function(event, task, title)
{
if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
rcmail.http_post(task + '/itip-remove',
{ uid: uid },
event,
rcmail.set_busy(true, 'itip.savingdata')
);
}

View file

@ -1310,6 +1310,9 @@ class libcalendaring extends rcube_plugin
$charset = $part->ctype_parameters['charset'] ?: RCMAIL_CHARSET;
$this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
// check if the parsed object is an instance of a recurring event/task
array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
// stop on the part that has an iTip method specified
if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
$this->mail_ical_parser->message_date = $this->ical_message->headers->date;
@ -1374,6 +1377,9 @@ class libcalendaring extends rcube_plugin
$object['_sender'] = preg_match(self::$email_regex, $headers->from, $m) ? $m[1] : '';
$object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
// check if this is an instance of a recurring event/task
self::identify_recurrence_instance($object);
return $object;
}
@ -1395,6 +1401,30 @@ class libcalendaring extends rcube_plugin
);
}
/**
* Single occourrences of recurring events are identified by their RECURRENCE-ID property
* in iCal which is represented as 'recurrence_date' in our internal data structure.
*
* Check if such a property exists and derive the '_instance' identifier and '_savemode'
* attributes which are used in the storage backend to identify the nested exception item.
*/
public static function identify_recurrence_instance(&$object)
{
// set instance and 'savemode' according to recurrence-id
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'] = $event['thisandfuture'] ? 'future' : 'current';
}
else if (!empty($object['recurrence_id']) || !empty($object['_instance'])) {
if (strlen($object['_instance']) > 4) {
$object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
}
else {
$object['recurrence_date'] = clone $object['start'];
}
}
}
/********* Attendee handling functions *********/

View file

@ -948,6 +948,13 @@ class libvcalendar implements Iterator
if (!empty($event['due']))
$ve->add($this->datetime_prop('DUE', $event['due'], false));
// we're exporting a recurrence instance only
if (!$recurrence_id && $event['recurrence_date'] && $event['recurrence_date'] instanceof DateTime) {
$recurrence_id = $this->datetime_prop('RECURRENCE-ID', $event['recurrence_date'], false, (bool)$event['allday']);
if ($event['thisandfuture'])
$recurrence_id->add('RANGE', 'THISANDFUTURE');
}
if ($recurrence_id)
$ve->add($recurrence_id);

View file

@ -108,6 +108,12 @@ $labels['acceptinvitation'] = 'Do you accept this invitation?';
$labels['acceptattendee'] = 'Accept participant';
$labels['declineattendee'] = 'Decline participant';
$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
$labels['rsvprecurringevent'] = 'This is a series of events! Does your response apply to all, this occurrence only or this and future occurrences?';
$labels['itipsingleoccurrence'] = 'This is a <em>single occurrence</em> out of a series of events';
$labels['itiprequestoccurrenceonly'] = 'The invitation only refers to this single occurrence';
$labels['itipreplyoccurrenceonly'] = 'The response only refers to this single occurrence';
$labels['itipcanceloccurrenceonly'] = 'The cancellation only refers to this single occurrence';
$labels['youhaveaccepted'] = 'You have accepted this invitation';
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';

View file

@ -87,9 +87,13 @@ class kolab_date_recurrence
$next_end->add($this->duration);
$next = $this->object->to_array();
$next['recurrence_id'] = $next_start->format('Y-m-d');
$next['start'] = $next_start;
$next['end'] = $next_end;
$recurrence_id_format = $next['allday'] ? 'Ymd' : 'Ymd\THis';
$next['recurrence_date'] = clone $next_start;
$next['_instance'] = $next_start->format($recurrence_id_format);
unset($next['_formatobj']);
return $next;

View file

@ -237,6 +237,7 @@ class kolab_format_event extends kolab_format_xcal
private function compact_exception($exception, $master)
{
$forbidden = array('recurrence','organizer','_attachments');
$whitelist = array('start','end');
foreach ($forbidden as $prop) {
if (array_key_exists($prop, $exception)) {
@ -245,7 +246,7 @@ class kolab_format_event extends kolab_format_xcal
}
foreach ($master as $prop => $value) {
if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value) {
if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value && !in_array($prop, $whitelist)) {
unset($exception[$prop]);
}
}