Merge branch 'dev/recurring-invitations'
This commit is contained in:
commit
52bbf63a8e
21 changed files with 1062 additions and 328 deletions
|
@ -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,10 +874,7 @@ 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;
|
||||
break;
|
||||
|
@ -879,10 +882,7 @@ class calendar extends rcube_plugin
|
|||
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')
|
||||
|
@ -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;
|
||||
|
@ -2405,9 +2469,12 @@ class calendar extends rcube_plugin
|
|||
{
|
||||
$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);
|
||||
$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,11 +2824,14 @@ 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))) {
|
||||
(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if delegatee has declined, set delegator's RSVP=True
|
||||
if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) {
|
||||
|
@ -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,8 +2904,44 @@ class calendar extends rcube_plugin
|
|||
if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
|
||||
$event['free_busy'] = 'free';
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -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 += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
|
||||
rcmail.gettext(message_label, 'calendar') + '</div>' +
|
||||
'<div class="savemode">' +
|
||||
'<a href="#current" class="button' + disabled_state + '">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
|
||||
'<a href="#future" class="button' + disabled_state + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
|
||||
'<a href="#current" class="button">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
|
||||
'<a href="#future" class="button' + future_disabled + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
|
||||
'<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
|
||||
(action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
|
||||
'</div>';
|
||||
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,6 +290,18 @@ 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;
|
||||
}
|
||||
|
@ -316,20 +309,34 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
// 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']) {
|
||||
// post-filter all events by fulltext search and partstat values
|
||||
$me = $this;
|
||||
$events = array_filter($events, function($event) use ($words, $me) {
|
||||
$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);
|
||||
}
|
||||
return $hits >= count($words);
|
||||
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);
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
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 2;
|
||||
}
|
||||
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,9 +1131,32 @@ class kolab_driver extends calendar_driver
|
|||
$event['end'] = $master['end'];
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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) &&
|
||||
is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
|
||||
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'] :
|
||||
|
@ -1010,6 +1168,7 @@ class kolab_driver extends calendar_driver
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unset _dateonly flags in (cached) date objects
|
||||
unset($event['start']->_dateonly, $event['end']->_dateonly);
|
||||
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
<div class="calwidth">
|
||||
<roundcube:object name="plugin.calendar_list" activeonly="true" id="calendarlist" />
|
||||
<br style="clear:both">
|
||||
</div>
|
||||
|
||||
<roundcube:object name="plugin.calendar_css" printmode="true" />
|
||||
|
|
|
@ -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']));
|
||||
}
|
||||
|
|
|
@ -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 = $('<ul></ul>').addClass('popupmenu libcal-rsvp-replymode');
|
||||
|
||||
$.each(['all','current'/*,'future'*/], function(i, mode) {
|
||||
$('<li><a>' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '</a>')
|
||||
.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')
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 *********/
|
||||
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <em>single occurrence</em> out of a series of events';
|
||||
$labels['itipfutureoccurrence'] = 'Refers to <em>this and all future occurrences</em> 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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
if ($this->check_rescheduling($object)) {
|
||||
$object['sequence']++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
Loading…
Add table
Reference in a new issue