diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 51381cb4..f6079db1 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -841,17 +841,21 @@ 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);
+ // load main event when savemode is 'all'
+ if ($event['_savemode'] == 'all' && $old['recurrence_id']) {
+ $old['id'] = $old['recurrence_id'];
+ $old = $this->driver->get_event($old);
+ }
+ }
+
switch ($action) {
case "new":
// create UID for new event
@@ -859,7 +863,9 @@ 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);
+ $this->event_save_success($event, null, $action, true);
}
$reload = $success && $event['recurrence'] ? 2 : 1;
break;
@@ -868,21 +874,15 @@ class calendar extends rcube_plugin
$this->write_preprocess($event, $action);
if ($success = $this->driver->edit_event($event)) {
$this->cleanup_event($event);
- if ($success !== true) {
- $event['id'] = $success;
- $old = null;
- }
+ $this->event_save_success($event, $old, $action, $success);
}
- $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
+ $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
break;
case "resize":
$this->write_preprocess($event, $action);
if ($success = $this->driver->resize_event($event)) {
- if ($success !== true) {
- $event['id'] = $success;
- $old = null;
- }
+ $this->event_save_success($event, $old, $action, $success);
}
$reload = $event['_savemode'] ? 2 : 1;
break;
@@ -890,10 +890,7 @@ class calendar extends rcube_plugin
case "move":
$this->write_preprocess($event, $action);
if ($success = $this->driver->move_event($event)) {
- if ($success !== true) {
- $event['id'] = $success;
- $old = null;
- }
+ $this->event_save_success($event, $old, $action, $success);
}
$reload = $success && $event['_savemode'] ? 2 : 1;
break;
@@ -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'))
@@ -947,6 +950,9 @@ class calendar extends rcube_plugin
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
+ else if ($success) {
+ $this->event_save_success($event, $old, $action, $success);
+ }
break;
case "undo":
@@ -967,18 +973,20 @@ class calendar extends rcube_plugin
case "rsvp":
$itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
- $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC);
+ $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST);
+ $attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST);
$reply_comment = $event['comment'];
$this->write_preprocess($event, 'edit');
$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']) {
$itip = $this->load_itip();
- if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'])) {
+ if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'], $attendees)) {
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
$noreply = false;
}
@@ -986,10 +994,15 @@ class calendar extends rcube_plugin
$event = $ev;
- if ($success = $this->driver->edit_rsvp($event, $status)) {
+ // compose a list of attendees affected by this change
+ $updated_attendees = array_filter(array_map(function($j) use ($event) {
+ return $event['attendees'][$j];
+ }, $attendees));
+
+ if ($success = $this->driver->edit_rsvp($event, $status, $updated_attendees)) {
$noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);
$noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
- $reload = $event['calendar'] != $ev['calendar'] ? 2 : 1;
+ $reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1;
$organizer = null;
$emails = $this->get_user_emails();
@@ -1006,11 +1019,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;
@@ -1122,27 +1142,6 @@ class calendar extends rcube_plugin
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
- // send out notifications
- if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) {
- // 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';
- }
-
- // only notify if data really changed (TODO: do diff check on client already)
- if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
- $sent = $this->notify_attendees($event, $old, $action, $event['_comment']);
- if ($sent > 0)
- $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
- else if ($sent < 0)
- $this->rc->output->show_message('calendar.errornotifying', 'error');
- }
- }
-
// unlock client
$this->rc->output->command('plugin.unlock_saving');
@@ -1157,6 +1156,62 @@ class calendar extends rcube_plugin
}
}
+ /**
+ * Helper method sending iTip notifications after successful event updates
+ */
+ private function event_save_success(&$event, $old, $action, $success)
+ {
+ // $success is a new event ID
+ if ($success !== true) {
+ // send update notification on the main event
+ if ($event['_savemode'] == 'future' && $event['_notify'] && $old['attendees'] && $old['recurrence_id']) {
+ $master = $this->driver->get_event(array('id' => $old['recurrence_id'], 'calendar' => $old['calendar']));
+ unset($master['_instance'], $master['recurrence_date']);
+
+ $sent = $this->notify_attendees($master, null, $action, $event['_comment']);
+ if ($sent < 0)
+ $this->rc->output->show_message('calendar.errornotifying', 'error');
+
+ $event['attendees'] = $master['attendees']; // this tricks us into the next if clause
+ }
+
+ $event['id'] = $success;
+ $event['_savemode'] = 'all';
+ $old = null;
+ }
+
+ // send out notifications
+ if ($event['_notify'] && ($event['attendees'] || $old['attendees'])) {
+ $_savemode = $event['_savemode'];
+
+ // send notification for the main event when savemode is 'all'
+ if ($action != 'remove' && $_savemode == 'all' && $old['recurrence_id']) {
+ $event['id'] = $old['recurrence_id'];
+ $event = $this->driver->get_event($event);
+ unset($event['_instance'], $event['recurrence_date']);
+ }
+ else {
+ // make sure we have the complete record
+ $event = $action == 'remove' ? $old : $this->driver->get_event($event);
+ }
+
+ $event['_savemode'] = $_savemode;
+
+ if ($old) {
+ $old['thisandfuture'] = $_savemode == 'future';
+ }
+
+ // only notify if data really changed (TODO: do diff check on client already)
+ if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
+ $sent = $this->notify_attendees($event, $old, $action, $event['_comment']);
+ if ($sent > 0)
+ $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+ else if ($sent < 0)
+ $this->rc->output->show_message('calendar.errornotifying', 'error');
+ }
+ }
+ }
+
/**
* Handler for load-requests from fullcalendar
* This will return pure JSON formatted output
@@ -1663,6 +1718,7 @@ class calendar extends rcube_plugin
if ($event['recurrence']) {
$event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
$event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
+ unset($event['recurrence_date']);
}
foreach ((array)$event['attachments'] as $k => $attachment) {
@@ -1687,6 +1743,9 @@ class calendar extends rcube_plugin
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) {
$event['attendees'][$i]['noreply'] = true;
}
+ else {
+ unset($event['attendees'][$i]['noreply']);
+ }
}
if ($organizer === null && !empty($event['organizer'])) {
@@ -1945,6 +2004,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']);
@@ -1987,6 +2049,8 @@ class calendar extends rcube_plugin
$sent = -100;
}
+ // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
+
// send CANCEL message to removed attendees
foreach ((array)$old['attendees'] as $attendee) {
if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current))
@@ -2212,7 +2276,7 @@ class calendar extends rcube_plugin
public static function event_diff($a, $b)
{
$diff = array();
- $ignore = array('changed' => 1, 'attachments' => 1, 'recurrence' => 1, '_notify' => 1, '_owner' => 1);
+ $ignore = array('changed' => 1, 'attachments' => 1, '_notify' => 1, '_owner' => 1, '_savemode' => 1);
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
if (!$ignore[$key] && $a[$key] != $b[$key])
$diff[] = $key;
@@ -2403,11 +2467,14 @@ class calendar extends rcube_plugin
*/
function event_itip_remove()
{
- $success = false;
- $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
+ $success = false;
+ $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
+ $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $savemode = rcube_utils::get_input_value('_savemode', 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' => $instance), true)) {
+ $event['_savemode'] = $savemode;
$success = $this->driver->remove_event($event, true);
}
@@ -2635,6 +2702,8 @@ class calendar extends rcube_plugin
$delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
$noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST));
$noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
+ $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
$error_msg = $this->gettext('errorimportingevent');
$success = false;
@@ -2722,12 +2791,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'] = $savemode ?: $event['_savemode'];
+
// only update attendee status
if ($event['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address
@@ -2740,9 +2810,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';
@@ -2752,9 +2824,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;
+ }
}
}
@@ -2771,12 +2846,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');
@@ -2829,7 +2904,43 @@ class calendar extends rcube_plugin
if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
$event['free_busy'] = 'free';
}
- $success = $this->driver->new_event($event);
+
+ // if the RSVP reply only refers to a single instance:
+ // store unmodified master event with current instance as exception
+ 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
+ if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) {
+ $recurrence_date->setTime(23,59,59);
+
+ foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) {
+ if ($recurring['_instance'] == $instance) {
+ // copy attendees block with my partstat to exception
+ $recurring['attendees'] = $event['attendees'];
+ $master['recurrence']['EXCEPTIONS'][] = $recurring;
+ $event = $recurring; // set reference for iTip reply
+ break;
+ }
+ }
+
+ $master['calendar'] = $event['calendar'] = $calendar['id'];
+ $success = $this->driver->new_event($master);
+ }
+ else {
+ $master = null;
+ }
+ }
+ else {
+ $master = null;
+ }
+ }
+
+ // save to the selected/default calendar
+ if (!$master) {
+ $event['calendar'] = $calendar['id'];
+ $success = $this->driver->new_event($event);
+ }
}
else if ($status == 'declined')
$error_msg = null;
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index c1e3d556..eb57fa71 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -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 .rsvp-buttons').addClass('recurring');
+ }
+ else {
+ $('#event-rsvp .rsvp-buttons').removeClass('recurring');
+ }
}
var buttons = [];
@@ -596,13 +604,16 @@ function rcube_calendar_ui(settings)
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
- rcmail.command('menu-close','eventoptionsmenu')
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
},
dragStart: function() {
- rcmail.command('menu-close','eventoptionsmenu')
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
},
resizeStart: function() {
- rcmail.command('menu-close','eventoptionsmenu')
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
},
buttons: buttons,
minWidth: 320,
@@ -683,6 +694,8 @@ function rcube_calendar_ui(settings)
// reset dialog first
$('#eventtabs').get(0).reset();
+ $('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', false);
+ $('#event-panel-recurrence, #event-panel-attachments').removeClass('disabled');
// allow other plugins to do actions when event form is opened
rcmail.triggerEvent('calendar-event-init', {o: event});
@@ -742,11 +755,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);
+ $('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true).change();
}
else
$('#edit-recurring-warning').hide();
@@ -797,7 +808,7 @@ function rcube_calendar_ui(settings)
// attachments
var load_attachments_tab = function()
{
- rcmail.enable_command('remove-attachment', !calendar.readonly);
+ rcmail.enable_command('remove-attachment', !calendar.readonly && !event.recurrence_id);
rcmail.env.deleted_attachments = [];
// we're sharing some code for uploads handling with app.js
rcmail.env.attachments = [];
@@ -2370,19 +2381,37 @@ function rcube_calendar_ui(settings)
}
// when the user accepts or declines an event invitation
- var event_rsvp = function(response, delegate)
+ var event_rsvp = function(response, delegate, replymode)
{
+ var btn;
+ if (typeof response == 'object') {
+ btn = $(response);
+ response = btn.attr('rel')
+ }
+ else {
+ btn = $('#event-rsvp input.button[rel='+response+']');
+ }
+
+ // show menu to select rsvp reply mode (current or all)
+ if (me.selected_event && me.selected_event.recurrence && !replymode) {
+ rcube_libcalendaring.itip_rsvp_recurring(btn, function(resp, mode) {
+ event_rsvp(resp, null, mode);
+ });
+ return;
+ }
+
if (me.selected_event && me.selected_event.attendees && response) {
// bring up delegation dialog
if (response == 'delegated' && !delegate) {
rcube_libcalendaring.itip_delegate_dialog(function(data) {
data.rsvp = data.rsvp ? 1 : '';
- event_rsvp('delegated', data);
+ event_rsvp('delegated', data, replymode);
});
return;
}
// update attendee status
+ attendees = [];
for (var data, i=0; i < me.selected_event.attendees.length; i++) {
data = me.selected_event.attendees[i];
if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) {
@@ -2391,6 +2420,7 @@ function rcube_calendar_ui(settings)
if (data.status == 'DELEGATED') {
data['delegated-to'] = delegate.to;
+ data.rsvp = delegate.rsvp
}
else {
if (data['delegated-to']) {
@@ -2399,6 +2429,12 @@ function rcube_calendar_ui(settings)
data.role = 'REQ-PARTICIPANT';
}
}
+
+ attendees.push(i)
+ }
+ else if (response != 'DELEGATED' && data['delegated-from'] &&
+ settings.identity.emails.indexOf(';'+String(data['delegated-from']).toLowerCase()) >= 0) {
+ delete data['delegated-from'];
}
// set free_busy status to transparent if declined (#4425)
@@ -2411,7 +2447,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: replymode || 'all' }, (delegate || {})),
noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
// import event from mail (temporary iTip event)
@@ -2425,15 +2461,17 @@ 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,
+ _instance: submit_data._instance,
+ _savemode: submit_data._savemode
});
}
else if (settings.invitation_calendars) {
- update_event('rsvp', submit_data, { status:response, noreply:noreply });
+ update_event('rsvp', submit_data, { status:response, noreply:noreply, attendees:attendees });
}
else {
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
- rcmail.http_post('event', { action:'rsvp', e:submit_data, status:response, noreply:noreply });
+ rcmail.http_post('event', { action:'rsvp', e:submit_data, status:response, attendees:attendees, noreply:noreply });
}
event_show_dialog(me.selected_event);
@@ -2501,7 +2539,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;
@@ -2565,25 +2603,19 @@ 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');
+ var future_disabled = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
- if (_has_attendees) {
- if (action == 'remove') {
- if (!_is_organizer) {
- message_label = 'removerecurringallonly';
- disabled_state = ' disabled';
- }
- }
- else if (is_organizer(event)) {
- disabled_state = ' disabled';
- }
+ // disable the 'future' savemode if attendees are involved
+ // reason: no calendaring system supports the thisandfuture range parameter
+ if (action == 'remove' && _has_attendees && is_organizer(event)) {
+ future_disabled = ' disabled';
}
html += '
' +
rcmail.gettext(message_label, 'calendar') + '
' +
'';
@@ -2606,8 +2638,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 +2656,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");
}
@@ -4151,9 +4185,16 @@ function rcube_calendar_ui(settings)
});
$('#event-rsvp input.button').click(function(e) {
- event_rsvp($(this).attr('rel'))
+ event_rsvp(this)
});
+ $('#eventedit input.edit-recurring-savemode').change(function(e) {
+ var sel = $('input.edit-recurring-savemode:checked').val(),
+ disabled = sel == 'current' || sel == 'future';
+ $('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', disabled);
+ $('#event-panel-recurrence, #event-panel-attachments')[(disabled?'addClass':'removeClass')]('disabled');
+ })
+
$('#eventshow .changersvp').click(function(e) {
var d = $('#eventshow'),
h = -$(this).closest('.event-line').toggle().height();
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 24e7a2e8..659f4a0e 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -50,6 +50,7 @@
* 'EXCEPTIONS' => array(), 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
@@ -196,9 +197,22 @@ abstract class calendar_driver
*
* @param array Hash array with event properties
* @param string New participant status
+ * @param array List of hash arrays with updated attendees
* @return boolean True on success, False on error
*/
- public function edit_rsvp(&$event, $status)
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ 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);
}
@@ -449,6 +463,7 @@ abstract class calendar_driver
$rcmail = rcmail::get_instance();
$recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
+ $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
// determine a reasonable end date if none given
if (!$end) {
@@ -464,12 +479,11 @@ abstract class calendar_driver
$i = 0;
while ($next_event = $recurrence->next_instance()) {
- $next_event['uid'] = $event['uid'] . '-' . ++$i;
// add to output if in range
if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
- $next_event['id'] = $next_event['uid'];
+ $next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
+ $next_event['id'] = $next_event['uid'] . '-' . $exception['_instance'];
$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
@@ -477,7 +491,7 @@ abstract class calendar_driver
}
// avoid endless recursion loops
- if ($i > 1000) {
+ if (++$i > 1000) {
break;
}
}
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 2b50ac8a..64bb0826 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);
}
}
@@ -236,35 +240,31 @@ class kolab_calendar extends kolab_storage_folder_api
$query[] = array('dtstart', '<=', $end);
$query[] = array('dtend', '>=', $start);
- // add query to exclude pending/declined invitations
- if (empty($filter_query) && $this->get_namespace() != 'other') {
- foreach ($user_emails as $email) {
- $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
- $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
- }
- }
- else if (is_array($filter_query)) {
+ if (is_array($filter_query)) {
$query = array_merge($query, $filter_query);
}
if (!empty($search)) {
$search = mb_strtolower($search);
+ $words = rcube_utils::tokenize_string($search, 1);
foreach (rcube_utils::normalize_string($search, true) as $word) {
$query[] = array('words', 'LIKE', $word);
}
}
+ else {
+ $words = array();
+ }
+
+ // set partstat filter to skip pending and declined invitations
+ if (empty($filter_query) && $this->get_namespace() != 'other') {
+ $partstat_exclude = array('NEEDS-ACTION','DECLINED');
+ }
+ else {
+ $partstat_exclude = array();
+ }
$events = array();
foreach ($this->storage->select($query) as $record) {
- // post-filter events to skip pending and declined invitations
- if (empty($filter_query) && is_array($record['attendees']) && $this->get_namespace() != 'other') {
- foreach ($record['attendees'] as $attendee) {
- if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], array('NEEDS-ACTION','DECLINED'))) {
- continue 2;
- }
- }
- }
-
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
@@ -272,35 +272,16 @@ class kolab_calendar extends kolab_storage_folder_api
if ($event['categories'])
$this->categories[$event['categories']]++;
- // filter events by search query
- if (!empty($search)) {
- $hits = 0;
- $words = rcube_utils::tokenize_string($search, 1);
- foreach ($words as $word) {
- $hits += $this->_fulltext_match($event, $word);
- }
-
- if ($hits < count($words)) // skip this event if not match with search term
- continue;
- }
-
// list events in requested time window
if ($event['start'] <= $end && $event['end'] >= $start) {
unset($event['_attendees']);
$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;
@@ -309,28 +290,54 @@ class kolab_calendar extends kolab_storage_folder_api
}
}
+ // find and merge exception for the first instance
+ if ($virtual && !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_exception_data($event, $exception);
+ }
+ }
+ }
+
if ($add)
$events[] = $event;
}
-
+
// resolve recurring events
if ($record['recurrence'] && $virtual == 1) {
$events = array_merge($events, $this->get_recurring_events($record, $start, $end));
-
- // when searching, only recurrence exceptions may match the query so post-filter the results again
- if (!empty($search) && $record['recurrence']['EXCEPTIONS']) {
- $me = $this;
- $events = array_filter($events, function($event) use ($words, $me) {
- $hits = 0;
- foreach ($words as $word) {
- $hits += $me->_fulltext_match($event, $word, false);
- }
- return $hits >= count($words);
- });
- }
}
}
+ // post-filter all events by fulltext search and partstat values
+ $me = $this;
+ $events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
+ // fulltext search
+ if (count($words)) {
+ $hits = 0;
+ foreach ($words as $word) {
+ $hits += $me->_fulltext_match($event, $word, false);
+ }
+ if ($hits < count($words)) {
+ return false;
+ }
+ }
+
+ // partstat filter
+ if (count($partstat_exclude) && is_array($event['attendees'])) {
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $partstat_exclude)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ });
+
// avoid session race conditions that will loose temporary subscriptions
$this->cal->rc->session->nowrite = true;
@@ -585,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,9 +639,10 @@ class kolab_calendar extends kolab_storage_folder_api
if (($next_event['start'] <= $end && $next_event['end'] >= $start) || ($event_id && $rec_id == $event_id)) {
$rec_event = $this->_to_rcube_event($next_event);
$rec_event['_instance'] = $instance_id;
+ $rec_event['_count'] = $i + 1;
if ($overlay_data || $exdata[$datestr]) // copy data from exception
- $this->_merge_event_data($rec_event, $exdata[$datestr] ?: $overlay_data);
+ kolab_driver::merge_exception_data($rec_event, $exdata[$datestr] ?: $overlay_data);
$rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['uid'];
@@ -653,38 +666,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','created','changed','recurrence','organizer','attendees','sequence');
-
- 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']);
@@ -702,38 +688,7 @@ class kolab_calendar extends kolab_storage_folder_api
*/
private function _from_rcube_event($event, $old = array())
{
- // in kolab_storage attachments are indexed by content-id
- $event['_attachments'] = array();
- if (is_array($event['attachments'])) {
- foreach ($event['attachments'] as $attachment) {
- $key = null;
- // Roundcube ID has nothing to do with the storage ID, remove it
- if ($attachment['content'] || $attachment['path']) {
- unset($attachment['id']);
- }
- else {
- foreach ((array)$old['_attachments'] as $cid => $oldatt) {
- if ($attachment['id'] == $oldatt['id'])
- $key = $cid;
- }
- }
-
- // flagged for deletion => set to false
- if ($attachment['_deleted']) {
- $event['_attachments'][$key] = false;
- }
- // replace existing entry
- else if ($key) {
- $event['_attachments'][$key] = $attachment;
- }
- // append as new attachment
- else {
- $event['_attachments'][] = $attachment;
- }
- }
-
- unset($event['attachments']);
- }
+ $event = kolab_driver::from_rcube_event($event, $old);
// set current user as ORGANIZER
$identity = $this->cal->rc->user->list_emails(true);
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 98318f25..88c2a384 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -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 instances
+ if (!$event['id'] && $event['_instance']) {
+ $id .= '-' . $event['_instance'];
+ }
}
else {
$id = $event;
@@ -614,9 +619,21 @@ class kolab_driver extends calendar_driver
* @param string New participant status
* @return boolean True on success, False on error
*/
- public function edit_rsvp(&$event, $status)
+ public function edit_rsvp(&$event, $status, $attendees)
{
- if (($ret = $this->update_event($event)) && $this->rc->config->get('kolab_invitation_calendars')) {
+ $update_event = $event;
+
+ // apply changes to master (and all exceptions)
+ if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
+ if ($storage = $this->get_calendar($event['calendar'])) {
+ $update_event = $storage->get_event($event['recurrence_id']);
+ $update_event['_savemode'] = $event['_savemode'];
+ unset($update_event['recurrence_id']);
+ self::merge_attendee_data($update_event, $attendees);
+ }
+ }
+
+ if (($ret = $this->update_attendees($update_event, $attendees)) && $this->rc->config->get('kolab_invitation_calendars')) {
// re-assign to the according (virtual) calendar
if (strtoupper($status) == 'DECLINED')
$event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
@@ -629,6 +646,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']) && !$event['recurrence_id'])) {
+ 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
@@ -640,6 +699,7 @@ class kolab_driver extends calendar_driver
{
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
+ self::clear_attandee_noreply($ev);
return $this->update_event($event + $ev);
}
@@ -656,6 +716,7 @@ class kolab_driver extends calendar_driver
{
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
+ self::clear_attandee_noreply($ev);
return $this->update_event($event + $ev);
}
@@ -687,11 +748,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 +941,12 @@ 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');
+ $object = $fromcalendar->storage->get_object($master['uid']);
+
+ // this-and-future on the first instance equals to 'all'
+ if (!$old['recurrence_id'] && $savemode == 'future')
+ $savemode = 'all';
}
// check if update affects scheduling and update attendee status accordingly
@@ -894,47 +960,115 @@ class kolab_driver extends calendar_driver
else if ($old['recurrence']['EXCEPTIONS'])
$event['recurrence']['EXCEPTIONS'] = $old['recurrence']['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'] = $object['_msguid'];
+ $event['_mailbox'] = $object['_mailbox'];
$event['uid'] = $this->cal->generate_uid();
- unset($event['recurrence_id'], $event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
+ unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
- // copy attachment data to new event
- foreach ((array)$event['attachments'] as $idx => $attachment) {
- if (!$attachment['content'])
- $event['attachments'][$idx]['content'] = $this->get_attachment_body($attachment['id'], $master);
- }
+ // copy attachment metadata to new event
+ $event = self::from_rcube_event($event, $object);
+ self::clear_attandee_noreply($event);
if ($success = $storage->insert_event($event))
$success = $event['uid'];
break;
case 'future':
+ // create a new recurring event
+ $event['_copyfrom'] = $object['_msguid'];
+ $event['_mailbox'] = $object['_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, $object);
+
+ // remove recurrence exceptions on re-scheduling
+ if ($reschedule) {
+ unset($event['recurrence']['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'];
+ });
+ }
+ }
+
+ // compute remaining occurrences
+ if ($event['recurrence']['COUNT']) {
+ if (!$old['_count'])
+ $old['_count'] = $this->get_recurrence_count($object, $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'];
+ });
+ }
+ 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!)
+ $master['sequence'] = $object['sequence'];
+ self::clear_attandee_noreply($master);
+ $udated = $storage->update_event($master);
+ }
+ break;
+
case 'current':
- // recurring instances shall not store recurrence rules
+ // 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'] = $old['sequence'] + 1;
+ $event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
+ }
+ else if (!isset($event['sequence'])) {
+ $event['sequence'] = $master['sequence'];
}
- // remove some internal properties which should not be saved
- unset($event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
-
// 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;
}
}
@@ -955,6 +1089,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'] ?: $old['start'];
$master['recurrence']['EXCEPTIONS'][] = $event;
}
$success = $storage->update_event($master);
@@ -996,17 +1131,41 @@ class kolab_driver extends calendar_driver
$event['end'] = $master['end'];
}
- // 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) {
- $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
+ // when saving an instance in 'all' mode, copy recurrence exceptions over
+ if ($old['recurrence_id']) {
+ $event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS'];
+ }
+
+ // 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) {
- $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);
+ self::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 = $event['allday'] ? 'Ymd' : 'Ymd\THis';
+ 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);
+ }
}
}
}
@@ -1029,26 +1188,14 @@ class kolab_driver extends calendar_driver
*/
public function check_scheduling(&$event, $old, $update = true)
{
- $reschedule = false;
-
// skip this check when importing iCal/iTip events
if (isset($event['sequence']) || !empty($event['_method'])) {
- return $reschedule;
+ return false;
}
// iterate through the list of properties considered 'significant' for scheduling
- foreach (kolab_format_event::$scheduling_properties as $prop) {
- $a = $old[$prop];
- $b = $event[$prop];
- if ($event['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
- $a = $a->format('Y-m-d');
- $b = $b->format('Y-m-d');
- }
- if ($a != $b) {
- $reschedule = true;
- break;
- }
- }
+ $kolab_event = $old['_formatobj'] ?: new kolab_format_event();
+ $reschedule = $kolab_event->check_rescheduling($event, $old);
// reset all attendee status to needs-action (#4360)
if ($update && $reschedule && is_array($event['attendees'])) {
@@ -1074,6 +1221,171 @@ 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;
+
+ // determine added and removed attendees
+ $added_attendees = $removed_attendees = array();
+ if ($savemode == 'future') {
+ $old_attendees = $current_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 ($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_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event, array('attendees'));
+
+ if (!empty($added_attendees) || !empty($removed_attendees)) {
+ self::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
+ }
+ }
+ }
+/*
+ // 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;
+ }
+
+ /**
+ * Remove the noreply flags from attendees
+ */
+ public static function clear_attandee_noreply(&$event)
+ {
+ foreach ((array)$event['attendees'] as $i => $attendee) {
+ unset($event['attendees'][$i]['noreply']);
+ }
+ }
+
+
+ /**
+ * 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
+ * @param array List of properties not allowed to be overwritten
+ */
+ public static function merge_exception_data(&$event, $overlay, $blacklist = null)
+ {
+ $forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
+
+ if (is_array($blacklist))
+ $forbidden = array_merge($forbidden, $blacklist);
+
+ // compute date offset from the exception
+ if ($overlay['start'] instanceof DateTime && $overlay['recurrence_date'] instanceof DateTime) {
+ $date_offset = $overlay['recurrence_date']->diff($overlay['start']);
+ }
+
+ foreach ($overlay as $prop => $value) {
+ if ($prop == 'start' || $prop == 'end') {
+ if (is_object($event[$prop]) && $event[$prop] instanceof DateTime) {
+ // 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')));
+ }
+ // apply date offset
+ else if ($date_offset) {
+ $event[$prop]->add($date_offset);
+ }
+ // adjust time of the recurring event instance
+ $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
+ }
+ }
+ 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, $removed = null)
+ {
+ 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;
+ }
+ }
+
+ // filter out removed attendees
+ if (!empty($removed)) {
+ $event['attendees'] = array_filter($event['attendees'], function($attendee) use ($removed) {
+ return !in_array($attendee['email'], $removed);
+ });
+ }
+ }
+
/**
* Get events from source.
*
@@ -1352,6 +1664,29 @@ class kolab_driver extends calendar_driver
return $storage->get_recurring_events($event, $start, $end);
}
+ /**
+ *
+ */
+ private function get_recurrence_count($event, $dtstart)
+ {
+ // use libkolab to compute recurring events
+ if (class_exists('kolabcalendaring') && $object['_formatobj']) {
+ $recurrence = new kolab_date_recurrence($object['_formatobj']);
+ }
+ else {
+ // fallback to local recurrence implementation
+ require_once($this->cal->home . '/lib/calendar_recurrence.php');
+ $recurrence = new calendar_recurrence($this->cal, $event);
+ }
+
+ $count = 0;
+ while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
+ $count++;
+ }
+
+ return $count;
+ }
+
/**
* Fetch free/busy information from a person within the given range
*/
@@ -1524,12 +1859,69 @@ class kolab_driver extends calendar_driver
if (empty($record['recurrence']))
unset($record['recurrence']);
+ // add instance identifier to first occurrence (master event)
+ // do not add 'recurrence_date' though in order to keep the master even being exported as such
+ if ($record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) {
+ $recurrence_id_format = $record['allday'] ? 'Ymd' : 'Ymd\THis';
+ $record['_instance'] = $record['start']->format($recurrence_id_format);
+ }
+
+ // clean up exception data
+ if (is_array($record['recurrence']['EXCEPTIONS'])) {
+ array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
+ unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
+ });
+ }
+
// remove internals
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
return $record;
}
+ /**
+ *
+ */
+ public static function from_rcube_event($event, $old = array())
+ {
+ // in kolab_storage attachments are indexed by content-id
+ if (is_array($event['attachments'])) {
+ $event['_attachments'] = array();
+
+ foreach ($event['attachments'] as $attachment) {
+ $key = null;
+ // Roundcube ID has nothing to do with the storage ID, remove it
+ if ($attachment['content'] || $attachment['path']) {
+ unset($attachment['id']);
+ }
+ else {
+ foreach ((array)$old['_attachments'] as $cid => $oldatt) {
+ if ($attachment['id'] == $oldatt['id'])
+ $key = $cid;
+ }
+ }
+
+ // flagged for deletion => set to false
+ if ($attachment['_deleted']) {
+ $event['_attachments'][$key] = false;
+ }
+ // replace existing entry
+ else if ($key) {
+ $event['_attachments'][$key] = $attachment;
+ }
+ // append as new attachment
+ else {
+ $event['_attachments'][] = $attachment;
+ }
+ }
+
+ unset($event['attachments']);
+ }
+
+ return $event;
+ }
+
+
/**
* Set CSS class according to the event's attendde partstat
*/
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index fae98bbe..d3af94dc 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -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;
diff --git a/plugins/calendar/print.js b/plugins/calendar/print.js
index c3c647ce..941d04c3 100644
--- a/plugins/calendar/print.js
+++ b/plugins/calendar/print.js
@@ -43,6 +43,9 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
var src, event_sources = [];
var add_url = (rcmail.env.search ? '&q='+escape(rcmail.env.search) : '');
for (var id in rcmail.env.calendars) {
+ if (!rcmail.env.calendars[id].active)
+ continue;
+
source = $.extend({
url: "./?_task=calendar&_action=load_events&source=" + escape(id) + add_url,
className: 'fc-event-cal-'+id,
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 50f8b645..fef16bd8 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -676,6 +676,10 @@ a.miniColors-trigger {
outline: none;
}
+#event-panel-attachments.disabled .attachmentslist li a.delete {
+ visibility: hidden;
+}
+
.event-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
@@ -1059,6 +1063,10 @@ td.topalign {
text-align: center;
}
+.libcal-rsvp-replymode li a {
+ cursor: default;
+}
+
#event-rsvp,
#edit-attendees-notify {
margin: 0.6em 0 0.3em 0;
@@ -2159,6 +2167,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;
diff --git a/plugins/calendar/skins/larry/print.css b/plugins/calendar/skins/larry/print.css
index 5d190f5b..fc5de975 100644
--- a/plugins/calendar/skins/larry/print.css
+++ b/plugins/calendar/skins/larry/print.css
@@ -1,7 +1,7 @@
/*** Printing styles for Calendar plugin ***/
body {
- margin: 0;
+ margin: 0 0 1em 0;
color: #000;
background: #fff;
}
@@ -54,18 +54,31 @@ body, td, th, div, p, h3, select, input, textarea {
}
#calendarlist {
- list-style-type: square;
+ list-style: none;
margin: 2em 0;
padding-left: 1em;
}
-#calendarlist li {
+#calendarlist ul {
+ float: left;
+ list-style: none;
padding-left: 0;
- padding-right: 3em;
+}
+
+#calendarlist li {
+ float: left;
+ padding-left: 0;
+ padding-right: 0;
margin-left: 0;
font-weight: bold;
}
+#calendarlist li div {
+ float: left;
+ padding-right: 3em;
+ padding-bottom: 1em;
+}
+
#calendarlist input,
#calendarlist .handle {
display: none;
@@ -207,6 +220,9 @@ body, td, th, div, p, h3, select, input, textarea {
font-style: italic;
}
+.fc-view-month .fc-event-hori .fc-event-inner {
+ background: #fff !important;
+}
.fc-view-table col.fc-event-location {
width: 20%;
diff --git a/plugins/calendar/skins/larry/templates/print.html b/plugins/calendar/skins/larry/templates/print.html
index 8d7789a0..e679f729 100644
--- a/plugins/calendar/skins/larry/templates/print.html
+++ b/plugins/calendar/skins/larry/templates/print.html
@@ -19,6 +19,7 @@
+
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 3eead6fa..14dacf4d 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -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($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **';
+ }
+ 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,15 +243,23 @@ 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') {
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['status'] != 'DELEGATED') {
- $event['attendees'][$i]['rsvp']= $rsvp ? true : null;
+ $event['attendees'][$i]['rsvp']= (bool)$rsvp;
}
}
}
+ else if ($method == 'CANCEL') {
+ if ($event['recurrence']) {
+ unset($event['recurrence']['EXCEPTIONS']);
+ }
+ }
// compose multipart message using PEAR:Mail_Mime
$message = new Mail_mime("\r\n");
@@ -276,9 +297,10 @@ class libcalendaring_itip
* @param array Event object to delegate
* @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
* @param boolean The delegator's RSVP flag
+ * @param array List with indexes of new/updated attendees
* @return boolean True on success, False on failure
*/
- public function delegate_to(&$event, $delegate, $rsvp = false)
+ public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array())
{
if (is_string($delegate)) {
$delegates = rcube_mime::decode_address_list($delegate, 1, false);
@@ -324,6 +346,8 @@ class libcalendaring_itip
$delegate_attendee['delegated-from'] = $me['email'];
$event['attendees'][$delegate_index] = $delegate_attendee;
+ $attendees[] = $delegate_index;
+
$this->set_sender_email($me['email']);
return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
}
@@ -343,7 +367,7 @@ class libcalendaring_itip
// check if the given itip object matches the last state
if ($existing) {
- $latest = (isset($event['sequence']) && $existing['sequence'] == $event['sequence']) ||
+ $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) ||
(!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']);
}
@@ -453,6 +477,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 +605,17 @@ 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'],
+ '_savemode' => $event['_savemode'],
+ ));
// 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 +676,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 +684,19 @@ class libcalendaring_itip
}
}
+ foreach (array('all','current','future') as $mode) {
+ $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode"));
+ }
+
+ $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('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
+ )
+ );
}
/**
@@ -705,7 +743,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($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence'));
+ }
+ else if (!empty($event['recurrence'])) {
$table->add('label', $this->gettext('recurring'));
$table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
}
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 0a2949d6..1c8fe16f 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -958,14 +958,48 @@ rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
return dialog;
};
+/**
+ * Show a menu for selecting the RSVP reply mode
+ */
+rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback)
+{
+ var mnu = $('').addClass('popupmenu libcal-rsvp-replymode');
+
+ $.each(['all','current'/*,'future'*/], function(i, mode) {
+ $('' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '')
+ .addClass('ui-menu-item')
+ .attr('rel', mode)
+ .appendTo(mnu);
+ });
+
+ var action = btn.attr('rel');
+
+ // open the mennu
+ mnu.menu({
+ select: function(event, ui) {
+ callback(action, ui.item.attr('rel'));
+ }
+ })
+ .appendTo(document.body)
+ .position({ my: 'left top', at: 'left bottom+2', of: btn })
+ .data('action', action);
+
+ setTimeout(function() {
+ $(document).one('click', function() {
+ mnu.menu('destroy');
+ mnu.remove();
+ });
+ }, 100);
+};
+
/**
*
*/
-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')
);
}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index f1c4514c..2d9c3cc7 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -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,34 @@ 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)
+ {
+ // for savemode=all, remove recurrence instance identifiers
+ if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) {
+ unset($object['_instance'], $object['recurrence_date']);
+ }
+ // set instance and 'savemode' according to recurrence-id
+ 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';
+ }
+ 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 *********/
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 4163cfbe..826e8d84 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -41,7 +41,7 @@ class libvcalendar implements Iterator
{
private $timezone;
private $attach_uri = null;
- private $prodid = '-//Roundcube//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
+ private $prodid = '-//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
private $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE',
'cutype' => 'CUTYPE', 'rsvp' => 'RSVP', 'delegated-from' => 'DELEGATED-FROM', 'delegated-to' => 'DELEGATED-TO');
@@ -64,7 +64,7 @@ class libvcalendar implements Iterator
function __construct($tz = null)
{
$this->timezone = $tz;
- $this->prodid = '-//Roundcube//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+ $this->prodid = '-//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
}
/**
@@ -502,7 +502,7 @@ class libvcalendar implements Iterator
case 'ATTENDEE':
case 'ORGANIZER':
- $params = array();
+ $params = array('rsvp' => false);
foreach ($prop->parameters as $param) {
switch ($param->name) {
case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
@@ -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);
@@ -1081,7 +1088,7 @@ class libvcalendar implements Iterator
}
else if (!empty($attendee['email'])) {
if (isset($attendee['rsvp']))
- $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : 'FALSE';
+ $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
$ve->add('ATTENDEE', 'mailto:' . $attendee['email'], array_filter(self::map_keys($attendee, $this->attendee_keymap)));
}
}
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 3e49838b..e5e04261 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -108,6 +108,14 @@ $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['rsvpmodeall'] = 'The entire series';
+$labels['rsvpmodecurrent'] = 'This occurrence only';
+$labels['rsvpmodefuture'] = 'This and future occurrences';
+
+$labels['itipsingleoccurrence'] = 'This is a single occurrence out of a series of events';
+$labels['itipfutureoccurrence'] = 'Refers to this and all future occurrences of a series of events';
+$labels['itipmessagesingleoccurrence'] = 'The message only refers to this single occurrence';
+$labels['itipmessagefutureoccurrence'] = 'The message refers to this and all future occurrences';
$labels['youhaveaccepted'] = 'You have accepted this invitation';
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist
index 6e4b613e..3a8476c3 100644
--- a/plugins/libkolab/config.inc.php.dist
+++ b/plugins/libkolab/config.inc.php.dist
@@ -41,7 +41,7 @@ $config['kolab_messages_cache_bypass'] = 0;
// These event properties contribute to a significant revision to the calendar component
// and if changed will increment the sequence number relevant for scheduling according to RFC 5545
-$config['kolab_event_scheduling_properties'] = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+$config['kolab_event_scheduling_properties'] = array('start', 'end', 'allday', 'recurrence', 'location', 'status', 'cancelled');
// These task properties contribute to a significant revision to the calendar component
// and if changed will increment the sequence number relevant for scheduling according to RFC 5545
diff --git a/plugins/libkolab/lib/kolab_date_recurrence.php b/plugins/libkolab/lib/kolab_date_recurrence.php
index 06dd3316..b2511f2b 100644
--- a/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -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;
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 075c5178..91efb26f 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -26,7 +26,7 @@ class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
- public static $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+ public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'status', 'cancelled');
protected $objclass = 'Event';
protected $read_func = 'readEvent';
@@ -44,6 +44,9 @@ class kolab_format_event extends kolab_format_xcal
$this->obj = $data;
$this->loaded = true;
}
+
+ // copy static property overriden by this class
+ $this->_scheduling_properties = self::$scheduling_properties;
}
/**
@@ -115,10 +118,13 @@ class kolab_format_event extends kolab_format_xcal
$vexceptions->push($exevent->obj);
// write cleaned-up exception data back to memory/cache
- $object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($compacted, $object);
+ $object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($exevent->data, $object);
}
$this->obj->setExceptions($vexceptions);
}
+ else if ($object['recurrence_date'] && $object['recurrence_date'] instanceof DateTime) {
+ $this->obj->setRecurrenceID(self::get_datetime($object['recurrence_date'], null, $object['allday']), (bool)$object['thisandfuture']);
+ }
// cache this data
$this->data = $object;
@@ -220,15 +226,16 @@ class kolab_format_event extends kolab_format_xcal
*
* @return array List of tags to save in cache
*/
- public function get_tags()
+ public function get_tags($obj = null)
{
- $tags = parent::get_tags();
+ $tags = parent::get_tags($obj);
+ $object = $obj ?: $this->data;
- foreach ((array)$this->data['categories'] as $cat) {
+ foreach ((array)$object['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
- return $tags;
+ return array_unique($tags);
}
/**
@@ -244,12 +251,6 @@ 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) {
- unset($exception[$prop]);
- }
- }
-
// preserve this property for date serialization
$exception['allday'] = $master['allday'];
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index ee0ca6a9..46408754 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -32,6 +32,16 @@ class kolab_format_task extends kolab_format_xcal
protected $read_func = 'readTodo';
protected $write_func = 'writeTodo';
+ /**
+ * Default constructor
+ */
+ function __construct($data = null, $version = 3.0)
+ {
+ parent::__construct(is_string($data) ? $data : null, $version);
+
+ // copy static property overriden by this class
+ $this->_scheduling_properties = self::$scheduling_properties;
+ }
/**
* Set properties to the kolabformat object
@@ -111,19 +121,21 @@ class kolab_format_task extends kolab_format_xcal
*
* @return array List of tags to save in cache
*/
- public function get_tags()
+ public function get_tags($obj = null)
{
- $tags = parent::get_tags();
+ $tags = parent::get_tags($obj);
+ $object = $obj ?: $this->data;
- if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
+ if ($object['status'] == 'COMPLETED' || ($object['complete'] == 100 && empty($object['status'])))
$tags[] = 'x-complete';
- if ($this->data['priority'] == 1)
+ if ($object['priority'] == 1)
$tags[] = 'x-flagged';
- if ($this->data['parent_id'])
- $tags[] = 'x-parent:' . $this->data['parent_id'];
+ if ($object['parent_id'])
+ $tags[] = 'x-parent:' . $object['parent_id'];
- return $tags;
+ return array_unique($tags);
}
+
}
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index d0f89b63..4d3a7583 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -32,6 +32,8 @@ abstract class kolab_format_xcal extends kolab_format
public static $scheduling_properties = array('start', 'end', 'location');
+ protected $_scheduling_properties = null;
+
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
@@ -317,21 +319,11 @@ abstract class kolab_format_xcal extends kolab_format
}
else {
$object['sequence'] = $old_sequence;
- $old = $this->data['uid'] ? $this->data : $this->to_array();
// increment sequence when updating properties relevant for scheduling.
// RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
- foreach (self::$scheduling_properties as $prop) {
- $a = $old[$prop];
- $b = $object[$prop];
- if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
- $a = $a->format('Y-m-d');
- $b = $b->format('Y-m-d');
- }
- if ($a != $b) {
- $object['sequence']++;
- break;
- }
+ if ($this->check_rescheduling($object)) {
+ $object['sequence']++;
}
}
}
@@ -365,7 +357,7 @@ abstract class kolab_format_xcal extends kolab_format
// set attendee RSVP if missing
if (!isset($attendee['rsvp'])) {
- $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = true;
+ $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = $reschedule;
}
$att = new Attendee;
@@ -619,22 +611,68 @@ abstract class kolab_format_xcal extends kolab_format
*
* @return array List of tags to save in cache
*/
- public function get_tags()
+ public function get_tags($obj = null)
{
$tags = array();
+ $object = $obj ?: $this->data;
- if (!empty($this->data['valarms'])) {
+ if (!empty($object['valarms'])) {
$tags[] = 'x-has-alarms';
}
// create tags reflecting participant status
- if (is_array($this->data['attendees'])) {
- foreach ($this->data['attendees'] as $attendee) {
+ if (is_array($object['attendees'])) {
+ foreach ($object['attendees'] as $attendee) {
if (!empty($attendee['email']) && !empty($attendee['status']))
$tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
}
}
- return $tags;
+ // collect tags from recurrence exceptions
+ if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
+ foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
+ $tags = array_merge($tags, $this->get_tags($exception));
+ }
+ }
+
+ return array_unique($tags);
+ }
+
+ /**
+ * Identify changes considered relevant for scheduling
+ *
+ * @param array Hash array with NEW object properties
+ * @param array Hash array with OLD object properties
+ *
+ * @return boolean True if changes affect scheduling, False otherwise
+ */
+ public function check_rescheduling($object, $old = null)
+ {
+ $reschedule = false;
+
+ if (!is_array($old)) {
+ $old = $this->data['uid'] ? $this->data : $this->to_array();
+ }
+
+ foreach ($this->_scheduling_properties ?: self::$scheduling_properties as $prop) {
+ $a = $old[$prop];
+ $b = $object[$prop];
+ if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+ $a = $a->format('Y-m-d');
+ $b = $b->format('Y-m-d');
+ }
+ if ($prop == 'recurrence' && is_array($a) && is_array($b)) {
+ unset($a['EXCEPTIONS']);
+ unset($b['EXCEPTIONS']);
+ $a = array_filter($a);
+ $b = array_filter($b);
+ }
+ if ($a != $b) {
+ $reschedule = true;
+ break;
+ }
+ }
+
+ return $reschedule;
}
}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index ab3c63f8..e0bf52e3 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -616,7 +616,8 @@ class kolab_storage_folder extends kolab_storage_folder_api
$type = $this->type;
// copy attachments from old message
- if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) {
+ $copyfrom = $object['_copyfrom'] ?: $object['_msguid'];
+ if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) {
foreach ((array)$old['_attachments'] as $key => $att) {
if (!isset($object['_attachments'][$key])) {
$object['_attachments'][$key] = $old['_attachments'][$key];
@@ -628,7 +629,7 @@ class kolab_storage_folder extends kolab_storage_folder_api
// load photo.attachment from old Kolab2 format to be directly embedded in xcard block
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
if (!isset($object['photo']))
- $object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
+ $object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']);
unset($object['_attachments'][$key]);
}
}
@@ -1010,7 +1011,7 @@ class kolab_storage_folder extends kolab_storage_folder_api
foreach ((array)$object['_attachments'] as $key => $att) {
if (empty($att['content']) && !empty($att['id'])) {
// @TODO: use IMAP CATENATE to skip attachment fetch+push operation
- $msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
+ $msguid = $object['_copyfrom'] ?: ($object['_msguid'] ?: $object['uid']);
if ($is_file) {
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {