summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugins/calendar/calendar.php43
-rw-r--r--plugins/calendar/calendar_ui.js31
-rw-r--r--plugins/calendar/drivers/calendar_driver.php2
-rw-r--r--plugins/calendar/drivers/kolab/kolab_calendar.php2
-rw-r--r--plugins/calendar/drivers/kolab/kolab_driver.php15
-rw-r--r--plugins/calendar/lib/calendar_recurrence.php5
-rw-r--r--plugins/calendar/skins/larry/calendar.css29
-rw-r--r--plugins/libcalendaring/lib/libcalendaring_itip.php55
-rw-r--r--plugins/libcalendaring/libcalendaring.js4
-rw-r--r--plugins/libcalendaring/libcalendaring.php30
-rw-r--r--plugins/libcalendaring/libvcalendar.php7
-rw-r--r--plugins/libcalendaring/localization/en_US.inc6
-rw-r--r--plugins/libkolab/lib/kolab_date_recurrence.php6
-rw-r--r--plugins/libkolab/lib/kolab_format_event.php3
14 files changed, 189 insertions, 49 deletions
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 51381cb..f1e66ec 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -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')
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index c1e3d55..1f9f399 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 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");
}
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 24e7a2e..e402db9 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -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
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 2b50ac8..15f030d 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -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
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 98318f2..1e3f0ea 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 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
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index fae98bb..d3af94d 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/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 50f8b64..1c2eca5 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -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;
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 3eead6f..53284a4 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('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']));
}
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 0a2949d..a13ebf7 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -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')
);
}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index f1c4514..c3bf625 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,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 *********/
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 4163cfb..10c2223 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -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);
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 3e49838..31f08fd 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -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';
diff --git a/plugins/libkolab/lib/kolab_date_recurrence.php b/plugins/libkolab/lib/kolab_date_recurrence.php
index 06dd331..b2511f2 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 075c517..bf17149 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -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]);
}
}