Reliably identify recurrence instances throughout the application to support invitations of recurring events (#4387)
This commit is contained in:
parent
a6ba6981e9
commit
78622133a9
14 changed files with 189 additions and 49 deletions
|
@ -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')
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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']));
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 *********/
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue