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);
|
$event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true);
|
||||||
$success = $reload = $got_msg = false;
|
$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
|
// 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;
|
$event['_notify'] = 1;
|
||||||
|
|
||||||
// read old event data in order to find changes
|
// 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);
|
$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) {
|
switch ($action) {
|
||||||
case "new":
|
case "new":
|
||||||
// create UID for new event
|
// create UID for new event
|
||||||
|
@ -859,7 +863,9 @@ class calendar extends rcube_plugin
|
||||||
$this->write_preprocess($event, $action);
|
$this->write_preprocess($event, $action);
|
||||||
if ($success = $this->driver->new_event($event)) {
|
if ($success = $this->driver->new_event($event)) {
|
||||||
$event['id'] = $event['uid'];
|
$event['id'] = $event['uid'];
|
||||||
|
$event['_savemode'] = 'all';
|
||||||
$this->cleanup_event($event);
|
$this->cleanup_event($event);
|
||||||
|
$this->event_save_success($event, null, $action, true);
|
||||||
}
|
}
|
||||||
$reload = $success && $event['recurrence'] ? 2 : 1;
|
$reload = $success && $event['recurrence'] ? 2 : 1;
|
||||||
break;
|
break;
|
||||||
|
@ -868,10 +874,7 @@ class calendar extends rcube_plugin
|
||||||
$this->write_preprocess($event, $action);
|
$this->write_preprocess($event, $action);
|
||||||
if ($success = $this->driver->edit_event($event)) {
|
if ($success = $this->driver->edit_event($event)) {
|
||||||
$this->cleanup_event($event);
|
$this->cleanup_event($event);
|
||||||
if ($success !== true) {
|
$this->event_save_success($event, $old, $action, $success);
|
||||||
$event['id'] = $success;
|
|
||||||
$old = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
|
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
|
||||||
break;
|
break;
|
||||||
|
@ -879,10 +882,7 @@ class calendar extends rcube_plugin
|
||||||
case "resize":
|
case "resize":
|
||||||
$this->write_preprocess($event, $action);
|
$this->write_preprocess($event, $action);
|
||||||
if ($success = $this->driver->resize_event($event)) {
|
if ($success = $this->driver->resize_event($event)) {
|
||||||
if ($success !== true) {
|
$this->event_save_success($event, $old, $action, $success);
|
||||||
$event['id'] = $success;
|
|
||||||
$old = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$reload = $event['_savemode'] ? 2 : 1;
|
$reload = $event['_savemode'] ? 2 : 1;
|
||||||
break;
|
break;
|
||||||
|
@ -890,10 +890,7 @@ class calendar extends rcube_plugin
|
||||||
case "move":
|
case "move":
|
||||||
$this->write_preprocess($event, $action);
|
$this->write_preprocess($event, $action);
|
||||||
if ($success = $this->driver->move_event($event)) {
|
if ($success = $this->driver->move_event($event)) {
|
||||||
if ($success !== true) {
|
$this->event_save_success($event, $old, $action, $success);
|
||||||
$event['id'] = $success;
|
|
||||||
$old = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$reload = $success && $event['_savemode'] ? 2 : 1;
|
$reload = $success && $event['_savemode'] ? 2 : 1;
|
||||||
break;
|
break;
|
||||||
|
@ -928,8 +925,14 @@ class calendar extends rcube_plugin
|
||||||
$got_msg = true;
|
$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
|
// send iTIP reply that participant has declined the event
|
||||||
if ($success && $event['decline']) {
|
if ($success && $event['_decline']) {
|
||||||
$emails = $this->get_user_emails();
|
$emails = $this->get_user_emails();
|
||||||
foreach ($old['attendees'] as $i => $attendee) {
|
foreach ($old['attendees'] as $i => $attendee) {
|
||||||
if ($attendee['role'] == 'ORGANIZER')
|
if ($attendee['role'] == 'ORGANIZER')
|
||||||
|
@ -947,6 +950,9 @@ class calendar extends rcube_plugin
|
||||||
else
|
else
|
||||||
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
|
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
|
||||||
}
|
}
|
||||||
|
else if ($success) {
|
||||||
|
$this->event_save_success($event, $old, $action, $success);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "undo":
|
case "undo":
|
||||||
|
@ -967,18 +973,20 @@ class calendar extends rcube_plugin
|
||||||
|
|
||||||
case "rsvp":
|
case "rsvp":
|
||||||
$itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
|
$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'];
|
$reply_comment = $event['comment'];
|
||||||
|
|
||||||
$this->write_preprocess($event, 'edit');
|
$this->write_preprocess($event, 'edit');
|
||||||
$ev = $this->driver->get_event($event);
|
$ev = $this->driver->get_event($event);
|
||||||
$ev['attendees'] = $event['attendees'];
|
$ev['attendees'] = $event['attendees'];
|
||||||
$ev['free_busy'] = $event['free_busy'];
|
$ev['free_busy'] = $event['free_busy'];
|
||||||
|
$ev['_savemode'] = $event['_savemode'];
|
||||||
|
|
||||||
// send invitation to delegatee + add it as attendee
|
// send invitation to delegatee + add it as attendee
|
||||||
if ($status == 'delegated' && $event['to']) {
|
if ($status == 'delegated' && $event['to']) {
|
||||||
$itip = $this->load_itip();
|
$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');
|
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
|
||||||
$noreply = false;
|
$noreply = false;
|
||||||
}
|
}
|
||||||
|
@ -986,10 +994,15 @@ class calendar extends rcube_plugin
|
||||||
|
|
||||||
$event = $ev;
|
$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 = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);
|
||||||
$noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
|
$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;
|
$organizer = null;
|
||||||
$emails = $this->get_user_emails();
|
$emails = $this->get_user_emails();
|
||||||
|
|
||||||
|
@ -1006,11 +1019,18 @@ class calendar extends rcube_plugin
|
||||||
$itip = $this->load_itip();
|
$itip = $this->load_itip();
|
||||||
$itip->set_sender_email($reply_sender);
|
$itip->set_sender_email($reply_sender);
|
||||||
$event['comment'] = $reply_comment;
|
$event['comment'] = $reply_comment;
|
||||||
|
$event['thisandfuture'] = $event['_savemode'] == 'future';
|
||||||
if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
|
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');
|
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
|
||||||
else
|
else
|
||||||
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
|
$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;
|
break;
|
||||||
|
|
||||||
|
@ -1122,27 +1142,6 @@ class calendar extends rcube_plugin
|
||||||
$this->rc->output->show_message('calendar.errorsaving', 'error');
|
$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
|
// unlock client
|
||||||
$this->rc->output->command('plugin.unlock_saving');
|
$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
|
* Handler for load-requests from fullcalendar
|
||||||
* This will return pure JSON formatted output
|
* This will return pure JSON formatted output
|
||||||
|
@ -1663,6 +1718,7 @@ class calendar extends rcube_plugin
|
||||||
if ($event['recurrence']) {
|
if ($event['recurrence']) {
|
||||||
$event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
|
$event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
|
||||||
$event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
|
$event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
|
||||||
|
unset($event['recurrence_date']);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ((array)$event['attachments'] as $k => $attachment) {
|
foreach ((array)$event['attachments'] as $k => $attachment) {
|
||||||
|
@ -1687,6 +1743,9 @@ class calendar extends rcube_plugin
|
||||||
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) {
|
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) {
|
||||||
$event['attendees'][$i]['noreply'] = true;
|
$event['attendees'][$i]['noreply'] = true;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
unset($event['attendees'][$i]['noreply']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($organizer === null && !empty($event['organizer'])) {
|
if ($organizer === null && !empty($event['organizer'])) {
|
||||||
|
@ -1945,6 +2004,9 @@ class calendar extends rcube_plugin
|
||||||
// add comment to the iTip attachment
|
// add comment to the iTip attachment
|
||||||
$event['comment'] = $comment;
|
$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
|
// compose multipart message using PEAR:Mail_Mime
|
||||||
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
|
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
|
||||||
$message = $itip->compose_itip_message($event, $method, $event['sequence'] > $old['sequence']);
|
$message = $itip->compose_itip_message($event, $method, $event['sequence'] > $old['sequence']);
|
||||||
|
@ -1987,6 +2049,8 @@ class calendar extends rcube_plugin
|
||||||
$sent = -100;
|
$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
|
// send CANCEL message to removed attendees
|
||||||
foreach ((array)$old['attendees'] as $attendee) {
|
foreach ((array)$old['attendees'] as $attendee) {
|
||||||
if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current))
|
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)
|
public static function event_diff($a, $b)
|
||||||
{
|
{
|
||||||
$diff = array();
|
$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) {
|
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
|
||||||
if (!$ignore[$key] && $a[$key] != $b[$key])
|
if (!$ignore[$key] && $a[$key] != $b[$key])
|
||||||
$diff[] = $key;
|
$diff[] = $key;
|
||||||
|
@ -2405,9 +2469,12 @@ class calendar extends rcube_plugin
|
||||||
{
|
{
|
||||||
$success = false;
|
$success = false;
|
||||||
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
|
$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
|
// 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);
|
$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));
|
$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 = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST));
|
||||||
$noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
|
$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');
|
$error_msg = $this->gettext('errorimportingevent');
|
||||||
$success = false;
|
$success = false;
|
||||||
|
@ -2722,12 +2791,13 @@ class calendar extends rcube_plugin
|
||||||
|
|
||||||
// save to calendar
|
// save to calendar
|
||||||
if ($calendar && !$calendar['readonly']) {
|
if ($calendar && !$calendar['readonly']) {
|
||||||
$event['calendar'] = $calendar['id'];
|
|
||||||
|
|
||||||
// check for existing event with the same UID
|
// 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) {
|
if ($existing) {
|
||||||
|
// forward savemode for correct updates of recurring events
|
||||||
|
$existing['_savemode'] = $savemode ?: $event['_savemode'];
|
||||||
|
|
||||||
// only update attendee status
|
// only update attendee status
|
||||||
if ($event['_method'] == 'REPLY') {
|
if ($event['_method'] == 'REPLY') {
|
||||||
// try to identify the attendee using the email sender address
|
// try to identify the attendee using the email sender address
|
||||||
|
@ -2740,9 +2810,11 @@ class calendar extends rcube_plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$event_attendee = null;
|
$event_attendee = null;
|
||||||
|
$update_attendees = array();
|
||||||
foreach ($event['attendees'] as $attendee) {
|
foreach ($event['attendees'] as $attendee) {
|
||||||
if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
|
if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
|
||||||
$event_attendee = $attendee;
|
$event_attendee = $attendee;
|
||||||
|
$update_attendees[] = $attendee;
|
||||||
$metadata['fallback'] = $attendee['status'];
|
$metadata['fallback'] = $attendee['status'];
|
||||||
$metadata['attendee'] = $attendee['email'];
|
$metadata['attendee'] = $attendee['email'];
|
||||||
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
|
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
|
||||||
|
@ -2752,11 +2824,14 @@ class calendar extends rcube_plugin
|
||||||
}
|
}
|
||||||
// also copy delegate attendee
|
// also copy delegate attendee
|
||||||
else if (!empty($attendee['delegated-from']) &&
|
else if (!empty($attendee['delegated-from']) &&
|
||||||
(stripos($attendee['delegated-from'], $event['_sender']) !== false || stripos($attendee['delegated-from'], $event['_sender_utf']) !== false) &&
|
(stripos($attendee['delegated-from'], $event['_sender']) !== false ||
|
||||||
(!in_array($attendee['email'], $existing_attendee_emails))) {
|
stripos($attendee['delegated-from'], $event['_sender_utf']) !== false)) {
|
||||||
|
$update_attendees[] = $attendee;
|
||||||
|
if (!in_array($attendee['email'], $existing_attendee_emails)) {
|
||||||
$existing['attendees'][] = $attendee;
|
$existing['attendees'][] = $attendee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if delegatee has declined, set delegator's RSVP=True
|
// if delegatee has declined, set delegator's RSVP=True
|
||||||
if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) {
|
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
|
// found matching attendee entry in both existing and new events
|
||||||
if ($existing_attendee >= 0 && $event_attendee) {
|
if ($existing_attendee >= 0 && $event_attendee) {
|
||||||
$existing['attendees'][$existing_attendee] = $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
|
// update the entire attendees block
|
||||||
else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
|
else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
|
||||||
$existing['attendees'][] = $event_attendee;
|
$existing['attendees'][] = $event_attendee;
|
||||||
$success = $this->driver->edit_event($existing);
|
$success = $this->driver->update_attendees($existing, $update_attendees);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$error_msg = $this->gettext('newerversionexists');
|
$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') {
|
if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
|
||||||
$event['free_busy'] = 'free';
|
$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);
|
$success = $this->driver->new_event($event);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if ($status == 'declined')
|
else if ($status == 'declined')
|
||||||
$error_msg = null;
|
$error_msg = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -554,6 +554,14 @@ function rcube_calendar_ui(settings)
|
||||||
|
|
||||||
$('#event-rsvp a.reply-comment-toggle').show();
|
$('#event-rsvp a.reply-comment-toggle').show();
|
||||||
$('#event-rsvp .itip-reply-comment textarea').hide().val('');
|
$('#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 = [];
|
var buttons = [];
|
||||||
|
@ -596,13 +604,16 @@ function rcube_calendar_ui(settings)
|
||||||
},
|
},
|
||||||
close: function() {
|
close: function() {
|
||||||
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
|
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
|
||||||
rcmail.command('menu-close','eventoptionsmenu')
|
rcmail.command('menu-close','eventoptionsmenu');
|
||||||
|
$('.libcal-rsvp-replymode').hide();
|
||||||
},
|
},
|
||||||
dragStart: function() {
|
dragStart: function() {
|
||||||
rcmail.command('menu-close','eventoptionsmenu')
|
rcmail.command('menu-close','eventoptionsmenu');
|
||||||
|
$('.libcal-rsvp-replymode').hide();
|
||||||
},
|
},
|
||||||
resizeStart: function() {
|
resizeStart: function() {
|
||||||
rcmail.command('menu-close','eventoptionsmenu')
|
rcmail.command('menu-close','eventoptionsmenu');
|
||||||
|
$('.libcal-rsvp-replymode').hide();
|
||||||
},
|
},
|
||||||
buttons: buttons,
|
buttons: buttons,
|
||||||
minWidth: 320,
|
minWidth: 320,
|
||||||
|
@ -683,6 +694,8 @@ function rcube_calendar_ui(settings)
|
||||||
|
|
||||||
// reset dialog first
|
// reset dialog first
|
||||||
$('#eventtabs').get(0).reset();
|
$('#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
|
// allow other plugins to do actions when event form is opened
|
||||||
rcmail.triggerEvent('calendar-event-init', {o: event});
|
rcmail.triggerEvent('calendar-event-init', {o: event});
|
||||||
|
@ -742,11 +755,9 @@ function rcube_calendar_ui(settings)
|
||||||
|
|
||||||
// show warning if editing a recurring event
|
// show warning if editing a recurring event
|
||||||
if (event.id && event.recurrence) {
|
if (event.id && event.recurrence) {
|
||||||
var allow_exceptions = !has_attendees(event) || !is_organizer(event),
|
var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
|
||||||
sel = event._savemode || (allow_exceptions && event.thisandfuture ? 'future' : (allow_exceptions && event.isexception ? 'current' : 'all'));
|
|
||||||
$('#edit-recurring-warning').show();
|
$('#edit-recurring-warning').show();
|
||||||
$('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true);
|
$('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true).change();
|
||||||
$('input.edit-recurring-savemode[value="current"], input.edit-recurring-savemode[value="future"]').prop('disabled', !allow_exceptions);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
$('#edit-recurring-warning').hide();
|
$('#edit-recurring-warning').hide();
|
||||||
|
@ -797,7 +808,7 @@ function rcube_calendar_ui(settings)
|
||||||
// attachments
|
// attachments
|
||||||
var load_attachments_tab = function()
|
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 = [];
|
rcmail.env.deleted_attachments = [];
|
||||||
// we're sharing some code for uploads handling with app.js
|
// we're sharing some code for uploads handling with app.js
|
||||||
rcmail.env.attachments = [];
|
rcmail.env.attachments = [];
|
||||||
|
@ -2370,19 +2381,37 @@ function rcube_calendar_ui(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// when the user accepts or declines an event invitation
|
// 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) {
|
if (me.selected_event && me.selected_event.attendees && response) {
|
||||||
// bring up delegation dialog
|
// bring up delegation dialog
|
||||||
if (response == 'delegated' && !delegate) {
|
if (response == 'delegated' && !delegate) {
|
||||||
rcube_libcalendaring.itip_delegate_dialog(function(data) {
|
rcube_libcalendaring.itip_delegate_dialog(function(data) {
|
||||||
data.rsvp = data.rsvp ? 1 : '';
|
data.rsvp = data.rsvp ? 1 : '';
|
||||||
event_rsvp('delegated', data);
|
event_rsvp('delegated', data, replymode);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update attendee status
|
// update attendee status
|
||||||
|
attendees = [];
|
||||||
for (var data, i=0; i < me.selected_event.attendees.length; i++) {
|
for (var data, i=0; i < me.selected_event.attendees.length; i++) {
|
||||||
data = me.selected_event.attendees[i];
|
data = me.selected_event.attendees[i];
|
||||||
if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) {
|
if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) {
|
||||||
|
@ -2391,6 +2420,7 @@ function rcube_calendar_ui(settings)
|
||||||
|
|
||||||
if (data.status == 'DELEGATED') {
|
if (data.status == 'DELEGATED') {
|
||||||
data['delegated-to'] = delegate.to;
|
data['delegated-to'] = delegate.to;
|
||||||
|
data.rsvp = delegate.rsvp
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (data['delegated-to']) {
|
if (data['delegated-to']) {
|
||||||
|
@ -2399,6 +2429,12 @@ function rcube_calendar_ui(settings)
|
||||||
data.role = 'REQ-PARTICIPANT';
|
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)
|
// set free_busy status to transparent if declined (#4425)
|
||||||
|
@ -2411,7 +2447,7 @@ function rcube_calendar_ui(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// submit status change to server
|
// 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;
|
noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
|
||||||
|
|
||||||
// import event from mail (temporary iTip event)
|
// import event from mail (temporary iTip event)
|
||||||
|
@ -2425,15 +2461,17 @@ function rcube_calendar_ui(settings)
|
||||||
_to: (delegate ? delegate.to : null),
|
_to: (delegate ? delegate.to : null),
|
||||||
_rsvp: (delegate && delegate.rsvp) ? 1 : 0,
|
_rsvp: (delegate && delegate.rsvp) ? 1 : 0,
|
||||||
_noreply: noreply,
|
_noreply: noreply,
|
||||||
_comment: submit_data.comment
|
_comment: submit_data.comment,
|
||||||
|
_instance: submit_data._instance,
|
||||||
|
_savemode: submit_data._savemode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (settings.invitation_calendars) {
|
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 {
|
else {
|
||||||
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
|
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);
|
event_show_dialog(me.selected_event);
|
||||||
|
@ -2501,7 +2539,7 @@ function rcube_calendar_ui(settings)
|
||||||
|
|
||||||
// mark all recurring instances as temp
|
// mark all recurring instances as temp
|
||||||
if (event.recurrence || event.recurrence_id) {
|
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) {
|
$.each(fc.fullCalendar('clientEvents', function(e){ return e.id == base_id || e.recurrence_id == base_id; }), function(i,ev) {
|
||||||
ev.temp = true;
|
ev.temp = true;
|
||||||
ev.editable = false;
|
ev.editable = false;
|
||||||
|
@ -2565,25 +2603,19 @@ function rcube_calendar_ui(settings)
|
||||||
|
|
||||||
// recurring event: user needs to select the savemode
|
// recurring event: user needs to select the savemode
|
||||||
if (event.recurrence) {
|
if (event.recurrence) {
|
||||||
var disabled_state = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
|
var future_disabled = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
|
||||||
|
|
||||||
if (_has_attendees) {
|
// disable the 'future' savemode if attendees are involved
|
||||||
if (action == 'remove') {
|
// reason: no calendaring system supports the thisandfuture range parameter
|
||||||
if (!_is_organizer) {
|
if (action == 'remove' && _has_attendees && is_organizer(event)) {
|
||||||
message_label = 'removerecurringallonly';
|
future_disabled = ' disabled';
|
||||||
disabled_state = ' disabled';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (is_organizer(event)) {
|
|
||||||
disabled_state = ' disabled';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
|
html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
|
||||||
rcmail.gettext(message_label, 'calendar') + '</div>' +
|
rcmail.gettext(message_label, 'calendar') + '</div>' +
|
||||||
'<div class="savemode">' +
|
'<div class="savemode">' +
|
||||||
'<a href="#current" class="button' + disabled_state + '">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
|
'<a href="#current" class="button">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
|
||||||
'<a href="#future" class="button' + disabled_state + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
|
'<a href="#future" class="button' + future_disabled + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
|
||||||
'<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
|
'<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
|
||||||
(action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
|
(action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
@ -2606,8 +2638,10 @@ function rcube_calendar_ui(settings)
|
||||||
else {
|
else {
|
||||||
if ($dialog.find('input.confirm-attendees-donotify').length)
|
if ($dialog.find('input.confirm-attendees-donotify').length)
|
||||||
data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
|
data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
|
||||||
if (decline && $dialog.find('input.confirm-attendees-decline:checked').length)
|
if (decline) {
|
||||||
data.decline = 1;
|
data._decline = $dialog.find('input.confirm-attendees-decline:checked').length;
|
||||||
|
data._notify = 0;
|
||||||
|
}
|
||||||
update_event(action, data);
|
update_event(action, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2622,7 +2656,7 @@ function rcube_calendar_ui(settings)
|
||||||
text: rcmail.gettext((action == 'remove' ? 'delete' : 'save'), 'calendar'),
|
text: rcmail.gettext((action == 'remove' ? 'delete' : 'save'), 'calendar'),
|
||||||
click: function() {
|
click: function() {
|
||||||
data._notify = notify && $dialog.find('input.confirm-attendees-donotify:checked').length ? 1 : 0;
|
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);
|
update_event(action, data);
|
||||||
$(this).dialog("close");
|
$(this).dialog("close");
|
||||||
}
|
}
|
||||||
|
@ -4151,9 +4185,16 @@ function rcube_calendar_ui(settings)
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#event-rsvp input.button').click(function(e) {
|
$('#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) {
|
$('#eventshow .changersvp').click(function(e) {
|
||||||
var d = $('#eventshow'),
|
var d = $('#eventshow'),
|
||||||
h = -$(this).closest('.event-line').toggle().height();
|
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
|
* '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
|
* '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',
|
* 'categories' => 'Event category',
|
||||||
* 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as
|
* 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as
|
||||||
* 'status' => 'TENTATIVE|CONFIRMED|CANCELLED', // event status according to RFC 2445
|
* '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 array Hash array with event properties
|
||||||
* @param string New participant status
|
* @param string New participant status
|
||||||
|
* @param array List of hash arrays with updated attendees
|
||||||
* @return boolean True on success, False on error
|
* @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);
|
return $this->edit_event($event);
|
||||||
}
|
}
|
||||||
|
@ -449,6 +463,7 @@ abstract class calendar_driver
|
||||||
|
|
||||||
$rcmail = rcmail::get_instance();
|
$rcmail = rcmail::get_instance();
|
||||||
$recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
|
$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
|
// determine a reasonable end date if none given
|
||||||
if (!$end) {
|
if (!$end) {
|
||||||
|
@ -464,12 +479,11 @@ abstract class calendar_driver
|
||||||
|
|
||||||
$i = 0;
|
$i = 0;
|
||||||
while ($next_event = $recurrence->next_instance()) {
|
while ($next_event = $recurrence->next_instance()) {
|
||||||
$next_event['uid'] = $event['uid'] . '-' . ++$i;
|
|
||||||
// add to output if in range
|
// add to output if in range
|
||||||
if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
|
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['recurrence_id'] = $event['uid'];
|
||||||
$next_event['_instance'] = $i;
|
|
||||||
$events[] = $next_event;
|
$events[] = $next_event;
|
||||||
}
|
}
|
||||||
else if ($next_event['start'] > $end) { // stop loop if out of range
|
else if ($next_event['start'] > $end) { // stop loop if out of range
|
||||||
|
@ -477,7 +491,7 @@ abstract class calendar_driver
|
||||||
}
|
}
|
||||||
|
|
||||||
// avoid endless recursion loops
|
// avoid endless recursion loops
|
||||||
if ($i > 1000) {
|
if (++$i > 1000) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,7 +195,11 @@ class kolab_calendar extends kolab_storage_folder_api
|
||||||
if ($master_id != $id && ($record = $this->storage->get_object($master_id)))
|
if ($master_id != $id && ($record = $this->storage->get_object($master_id)))
|
||||||
$this->events[$master_id] = $this->_to_rcube_event($record);
|
$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);
|
$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('dtstart', '<=', $end);
|
||||||
$query[] = array('dtend', '>=', $start);
|
$query[] = array('dtend', '>=', $start);
|
||||||
|
|
||||||
// add query to exclude pending/declined invitations
|
if (is_array($filter_query)) {
|
||||||
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)) {
|
|
||||||
$query = array_merge($query, $filter_query);
|
$query = array_merge($query, $filter_query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($search)) {
|
if (!empty($search)) {
|
||||||
$search = mb_strtolower($search);
|
$search = mb_strtolower($search);
|
||||||
|
$words = rcube_utils::tokenize_string($search, 1);
|
||||||
foreach (rcube_utils::normalize_string($search, true) as $word) {
|
foreach (rcube_utils::normalize_string($search, true) as $word) {
|
||||||
$query[] = array('words', 'LIKE', $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();
|
$events = array();
|
||||||
foreach ($this->storage->select($query) as $record) {
|
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);
|
$event = $this->_to_rcube_event($record);
|
||||||
$this->events[$event['id']] = $event;
|
$this->events[$event['id']] = $event;
|
||||||
|
|
||||||
|
@ -272,35 +272,16 @@ class kolab_calendar extends kolab_storage_folder_api
|
||||||
if ($event['categories'])
|
if ($event['categories'])
|
||||||
$this->categories[$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
|
// list events in requested time window
|
||||||
if ($event['start'] <= $end && $event['end'] >= $start) {
|
if ($event['start'] <= $end && $event['end'] >= $start) {
|
||||||
unset($event['_attendees']);
|
unset($event['_attendees']);
|
||||||
$add = true;
|
$add = true;
|
||||||
|
|
||||||
// skip the first instance of a recurring event if listed in exdate
|
// 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');
|
$event_date = $event['start']->format('Ymd');
|
||||||
$exdates = (array)$event['recurrence']['EXDATE'];
|
$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) {
|
foreach ($exdates as $exdate) {
|
||||||
if ($exdate->format('Ymd') == $event_date) {
|
if ($exdate->format('Ymd') == $event_date) {
|
||||||
$add = false;
|
$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)
|
if ($add)
|
||||||
$events[] = $event;
|
$events[] = $event;
|
||||||
}
|
}
|
||||||
|
@ -316,20 +309,34 @@ class kolab_calendar extends kolab_storage_folder_api
|
||||||
// resolve recurring events
|
// resolve recurring events
|
||||||
if ($record['recurrence'] && $virtual == 1) {
|
if ($record['recurrence'] && $virtual == 1) {
|
||||||
$events = array_merge($events, $this->get_recurring_events($record, $start, $end));
|
$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
|
// post-filter all events by fulltext search and partstat values
|
||||||
if (!empty($search) && $record['recurrence']['EXCEPTIONS']) {
|
|
||||||
$me = $this;
|
$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;
|
$hits = 0;
|
||||||
foreach ($words as $word) {
|
foreach ($words as $word) {
|
||||||
$hits += $me->_fulltext_match($event, $word, false);
|
$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
|
// avoid session race conditions that will loose temporary subscriptions
|
||||||
$this->cal->rc->session->nowrite = true;
|
$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['id'] = $event['uid'] . '-' . $exception['_instance'];
|
||||||
$rec_event['isexception'] = 1;
|
$rec_event['isexception'] = 1;
|
||||||
|
|
||||||
// found the specifically requested instance, exiting...
|
// found the specifically requested instance: register exception (single occurrence wins)
|
||||||
if ($rec_event['id'] == $event_id) {
|
if ($rec_event['id'] == $event_id && (!$this->events[$event_id] || $this->events[$event_id]['thisandfuture'])) {
|
||||||
$rec_event['recurrence'] = $recurrence_rule;
|
$rec_event['recurrence'] = $recurrence_rule;
|
||||||
$rec_event['recurrence_id'] = $event['uid'];
|
$rec_event['recurrence_id'] = $event['uid'];
|
||||||
$events[] = $rec_event;
|
|
||||||
$this->events[$rec_event['id']] = $rec_event;
|
$this->events[$rec_event['id']] = $rec_event;
|
||||||
return $events;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// remember this exception's date
|
// remember this exception's date
|
||||||
$exdate = substr($exception['_instance'], 0, 8);
|
$exdate = substr($exception['_instance'], 0, 8);
|
||||||
|
if (!$exdata[$exdate] || $exdata[$exdate]['thisandfuture']) {
|
||||||
$exdata[$exdate] = $rec_event;
|
$exdata[$exdate] = $rec_event;
|
||||||
|
}
|
||||||
if ($rec_event['thisandfuture']) {
|
if ($rec_event['thisandfuture']) {
|
||||||
$futuredata[$exdate] = $rec_event;
|
$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
|
// use libkolab to compute recurring events
|
||||||
if (class_exists('kolabcalendaring')) {
|
if (class_exists('kolabcalendaring')) {
|
||||||
$recurrence = new kolab_date_recurrence($object);
|
$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)) {
|
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 = $this->_to_rcube_event($next_event);
|
||||||
$rec_event['_instance'] = $instance_id;
|
$rec_event['_instance'] = $instance_id;
|
||||||
|
$rec_event['_count'] = $i + 1;
|
||||||
|
|
||||||
if ($overlay_data || $exdata[$datestr]) // copy data from exception
|
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['id'] = $rec_id;
|
||||||
$rec_event['recurrence_id'] = $event['uid'];
|
$rec_event['recurrence_id'] = $event['uid'];
|
||||||
|
@ -653,38 +666,11 @@ class kolab_calendar extends kolab_storage_folder_api
|
||||||
return $events;
|
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
|
* Convert from Kolab_Format to internal representation
|
||||||
*/
|
*/
|
||||||
private function _to_rcube_event($record)
|
private function _to_rcube_event($record)
|
||||||
{
|
{
|
||||||
$record['id'] = $record['uid'];
|
|
||||||
$record['calendar'] = $this->id;
|
$record['calendar'] = $this->id;
|
||||||
$record['links'] = $this->get_links($record['uid']);
|
$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())
|
private function _from_rcube_event($event, $old = array())
|
||||||
{
|
{
|
||||||
// in kolab_storage attachments are indexed by content-id
|
$event = kolab_driver::from_rcube_event($event, $old);
|
||||||
$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']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set current user as ORGANIZER
|
// set current user as ORGANIZER
|
||||||
$identity = $this->cal->rc->user->list_emails(true);
|
$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)
|
public function get_event($event, $writeable = false, $active = false, $personal = false)
|
||||||
{
|
{
|
||||||
if (is_array($event)) {
|
if (is_array($event)) {
|
||||||
$id = $event['id'] ? $event['id'] : $event['uid'];
|
$id = $event['id'] ?: $event['uid'];
|
||||||
$cal = $event['calendar'];
|
$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 {
|
else {
|
||||||
$id = $event;
|
$id = $event;
|
||||||
|
@ -614,9 +619,21 @@ class kolab_driver extends calendar_driver
|
||||||
* @param string New participant status
|
* @param string New participant status
|
||||||
* @return boolean True on success, False on error
|
* @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
|
// re-assign to the according (virtual) calendar
|
||||||
if (strtoupper($status) == 'DECLINED')
|
if (strtoupper($status) == 'DECLINED')
|
||||||
$event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
|
$event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
|
||||||
|
@ -629,6 +646,48 @@ class kolab_driver extends calendar_driver
|
||||||
return $ret;
|
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
|
* 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']))) {
|
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
|
||||||
unset($ev['sequence']);
|
unset($ev['sequence']);
|
||||||
|
self::clear_attandee_noreply($ev);
|
||||||
return $this->update_event($event + $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']))) {
|
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
|
||||||
unset($ev['sequence']);
|
unset($ev['sequence']);
|
||||||
|
self::clear_attandee_noreply($ev);
|
||||||
return $this->update_event($event + $ev);
|
return $this->update_event($event + $ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,11 +748,11 @@ class kolab_driver extends calendar_driver
|
||||||
// read master if deleting a recurring event
|
// read master if deleting a recurring event
|
||||||
if ($event['recurrence'] || $event['recurrence_id']) {
|
if ($event['recurrence'] || $event['recurrence_id']) {
|
||||||
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
|
$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
|
// 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) {
|
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
|
||||||
if ($exception['_instance'] == $event['_instance']) {
|
if ($exception['_instance'] == $event['_instance']) {
|
||||||
unset($master['recurrence']['EXCEPTIONS'][$i]);
|
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
|
// modify a recurring event, check submitted savemode to do the right things
|
||||||
if ($old['recurrence'] || $old['recurrence_id']) {
|
if ($old['recurrence'] || $old['recurrence_id']) {
|
||||||
$master = $old['recurrence_id'] ? $fromcalendar->get_event($old['recurrence_id']) : $old;
|
$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
|
// 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'])
|
else if ($old['recurrence']['EXCEPTIONS'])
|
||||||
$event['recurrence']['EXCEPTIONS'] = $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) {
|
switch ($savemode) {
|
||||||
case 'new':
|
case 'new':
|
||||||
// save submitted data as new (non-recurring) event
|
// save submitted data as new (non-recurring) event
|
||||||
$event['recurrence'] = array();
|
$event['recurrence'] = array();
|
||||||
|
$event['_copyfrom'] = $object['_msguid'];
|
||||||
|
$event['_mailbox'] = $object['_mailbox'];
|
||||||
$event['uid'] = $this->cal->generate_uid();
|
$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
|
// copy attachment metadata to new event
|
||||||
foreach ((array)$event['attachments'] as $idx => $attachment) {
|
$event = self::from_rcube_event($event, $object);
|
||||||
if (!$attachment['content'])
|
|
||||||
$event['attachments'][$idx]['content'] = $this->get_attachment_body($attachment['id'], $master);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
self::clear_attandee_noreply($event);
|
||||||
if ($success = $storage->insert_event($event))
|
if ($success = $storage->insert_event($event))
|
||||||
$success = $event['uid'];
|
$success = $event['uid'];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'future':
|
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':
|
case 'current':
|
||||||
// recurring instances shall not store recurrence rules
|
// recurring instances shall not store recurrence rules and attachments
|
||||||
$event['recurrence'] = array();
|
$event['recurrence'] = array();
|
||||||
$event['thisandfuture'] = $savemode == 'future';
|
$event['thisandfuture'] = $savemode == 'future';
|
||||||
|
unset($event['attachments'], $event['id']);
|
||||||
|
|
||||||
// increment sequence of this instance if scheduling is affected
|
// increment sequence of this instance if scheduling is affected
|
||||||
if ($reschedule) {
|
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
|
// save properties to a recurrence exception instance
|
||||||
if ($old['recurrence_id'] && is_array($master['recurrence']['EXCEPTIONS'])) {
|
if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) {
|
||||||
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
|
if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) {
|
||||||
if ($exception['_instance'] == $old['_instance']) {
|
|
||||||
$event['_instance'] = $old['_instance'];
|
|
||||||
$event['recurrence_date'] = $old['recurrence_date'];
|
|
||||||
$master['recurrence']['EXCEPTIONS'][$i] = $event;
|
|
||||||
$success = $storage->update_event($master, $old['id']);
|
$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
|
// save as new exception to master event
|
||||||
if ($add_exception) {
|
if ($add_exception) {
|
||||||
$event['_instance'] = $old['_instance'];
|
$event['_instance'] = $old['_instance'];
|
||||||
|
$event['recurrence_date'] = $old['recurrence_date'] ?: $old['start'];
|
||||||
$master['recurrence']['EXCEPTIONS'][] = $event;
|
$master['recurrence']['EXCEPTIONS'][] = $event;
|
||||||
}
|
}
|
||||||
$success = $storage->update_event($master);
|
$success = $storage->update_event($master);
|
||||||
|
@ -996,9 +1131,32 @@ class kolab_driver extends calendar_driver
|
||||||
$event['end'] = $master['end'];
|
$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
|
// 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) &&
|
if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) {
|
||||||
is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
|
|
||||||
$recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
|
$recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
|
||||||
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
|
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
|
||||||
$recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
|
$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 _dateonly flags in (cached) date objects
|
||||||
unset($event['start']->_dateonly, $event['end']->_dateonly);
|
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)
|
public function check_scheduling(&$event, $old, $update = true)
|
||||||
{
|
{
|
||||||
$reschedule = false;
|
|
||||||
|
|
||||||
// skip this check when importing iCal/iTip events
|
// skip this check when importing iCal/iTip events
|
||||||
if (isset($event['sequence']) || !empty($event['_method'])) {
|
if (isset($event['sequence']) || !empty($event['_method'])) {
|
||||||
return $reschedule;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterate through the list of properties considered 'significant' for scheduling
|
// iterate through the list of properties considered 'significant' for scheduling
|
||||||
foreach (kolab_format_event::$scheduling_properties as $prop) {
|
$kolab_event = $old['_formatobj'] ?: new kolab_format_event();
|
||||||
$a = $old[$prop];
|
$reschedule = $kolab_event->check_rescheduling($event, $old);
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset all attendee status to needs-action (#4360)
|
// reset all attendee status to needs-action (#4360)
|
||||||
if ($update && $reschedule && is_array($event['attendees'])) {
|
if ($update && $reschedule && is_array($event['attendees'])) {
|
||||||
|
@ -1074,6 +1221,171 @@ class kolab_driver extends calendar_driver
|
||||||
return $reschedule;
|
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.
|
* Get events from source.
|
||||||
*
|
*
|
||||||
|
@ -1352,6 +1664,29 @@ class kolab_driver extends calendar_driver
|
||||||
return $storage->get_recurring_events($event, $start, $end);
|
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
|
* 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']))
|
if (empty($record['recurrence']))
|
||||||
unset($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
|
// remove internals
|
||||||
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
|
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
|
||||||
|
|
||||||
return $record;
|
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
|
* 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()) {
|
if ($next_start = $this->next()) {
|
||||||
$next = $this->event;
|
$next = $this->event;
|
||||||
$next['recurrence_id'] = $next_start->format('Y-m-d');
|
|
||||||
$next['start'] = $next_start;
|
$next['start'] = $next_start;
|
||||||
|
|
||||||
if ($this->duration) {
|
if ($this->duration) {
|
||||||
|
@ -75,6 +74,10 @@ class calendar_recurrence extends libcalendaring_recurrence
|
||||||
$next['end']->add($this->duration);
|
$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']);
|
unset($next['_formatobj']);
|
||||||
|
|
||||||
return $next;
|
return $next;
|
||||||
|
|
|
@ -43,6 +43,9 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
||||||
var src, event_sources = [];
|
var src, event_sources = [];
|
||||||
var add_url = (rcmail.env.search ? '&q='+escape(rcmail.env.search) : '');
|
var add_url = (rcmail.env.search ? '&q='+escape(rcmail.env.search) : '');
|
||||||
for (var id in rcmail.env.calendars) {
|
for (var id in rcmail.env.calendars) {
|
||||||
|
if (!rcmail.env.calendars[id].active)
|
||||||
|
continue;
|
||||||
|
|
||||||
source = $.extend({
|
source = $.extend({
|
||||||
url: "./?_task=calendar&_action=load_events&source=" + escape(id) + add_url,
|
url: "./?_task=calendar&_action=load_events&source=" + escape(id) + add_url,
|
||||||
className: 'fc-event-cal-'+id,
|
className: 'fc-event-cal-'+id,
|
||||||
|
|
|
@ -676,6 +676,10 @@ a.miniColors-trigger {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#event-panel-attachments.disabled .attachmentslist li a.delete {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.event-attendees span.attendee {
|
.event-attendees span.attendee {
|
||||||
padding-right: 18px;
|
padding-right: 18px;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
|
@ -1059,6 +1063,10 @@ td.topalign {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.libcal-rsvp-replymode li a {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
#event-rsvp,
|
#event-rsvp,
|
||||||
#edit-attendees-notify {
|
#edit-attendees-notify {
|
||||||
margin: 0.6em 0 0.3em 0;
|
margin: 0.6em 0 0.3em 0;
|
||||||
|
@ -2159,6 +2167,15 @@ div.calendar-invitebox td.sensitivity {
|
||||||
font-weight: bold;
|
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,
|
#event-rsvp .rsvp-buttons,
|
||||||
div.calendar-invitebox .itip-buttons div {
|
div.calendar-invitebox .itip-buttons div {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*** Printing styles for Calendar plugin ***/
|
/*** Printing styles for Calendar plugin ***/
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0 0 1em 0;
|
||||||
color: #000;
|
color: #000;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
@ -54,18 +54,31 @@ body, td, th, div, p, h3, select, input, textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
#calendarlist {
|
#calendarlist {
|
||||||
list-style-type: square;
|
list-style: none;
|
||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#calendarlist li {
|
#calendarlist ul {
|
||||||
|
float: left;
|
||||||
|
list-style: none;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 3em;
|
}
|
||||||
|
|
||||||
|
#calendarlist li {
|
||||||
|
float: left;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#calendarlist li div {
|
||||||
|
float: left;
|
||||||
|
padding-right: 3em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
#calendarlist input,
|
#calendarlist input,
|
||||||
#calendarlist .handle {
|
#calendarlist .handle {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -207,6 +220,9 @@ body, td, th, div, p, h3, select, input, textarea {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fc-view-month .fc-event-hori .fc-event-inner {
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.fc-view-table col.fc-event-location {
|
.fc-view-table col.fc-event-location {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
<div class="calwidth">
|
<div class="calwidth">
|
||||||
<roundcube:object name="plugin.calendar_list" activeonly="true" id="calendarlist" />
|
<roundcube:object name="plugin.calendar_list" activeonly="true" id="calendarlist" />
|
||||||
|
<br style="clear:both">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<roundcube:object name="plugin.calendar_css" printmode="true" />
|
<roundcube:object name="plugin.calendar_css" printmode="true" />
|
||||||
|
|
|
@ -98,8 +98,10 @@ class libcalendaring_itip
|
||||||
if (!$this->sender['name'])
|
if (!$this->sender['name'])
|
||||||
$this->sender['name'] = $this->sender['email'];
|
$this->sender['name'] = $this->sender['email'];
|
||||||
|
|
||||||
if (!$message)
|
if (!$message) {
|
||||||
|
libcalendaring::identify_recurrence_instance($event);
|
||||||
$message = $this->compose_itip_message($event, $method, $rsvp);
|
$message = $this->compose_itip_message($event, $method, $rsvp);
|
||||||
|
}
|
||||||
|
|
||||||
$mailto = rcube_idn_to_ascii($recipient['email']);
|
$mailto = rcube_idn_to_ascii($recipient['email']);
|
||||||
|
|
||||||
|
@ -121,12 +123,19 @@ class libcalendaring_itip
|
||||||
($attendee['name'] ? $attendee['name'] : $attendee['email']);
|
($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(
|
$mailbody = $this->gettext(array(
|
||||||
'name' => $bodytext,
|
'name' => $bodytext,
|
||||||
'vars' => array(
|
'vars' => array(
|
||||||
'title' => $event['title'],
|
'title' => $event['title'],
|
||||||
'date' => $this->lib->event_date_text($event, true) .
|
'date' => $this->lib->event_date_text($event, true) . $recurrence_info,
|
||||||
(empty($event['recurrence']) ? '' : sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']))),
|
|
||||||
'attendees' => join(",\n ", $attendees_list),
|
'attendees' => join(",\n ", $attendees_list),
|
||||||
'sender' => $this->sender['name'],
|
'sender' => $this->sender['name'],
|
||||||
'organizer' => $this->sender['name'],
|
'organizer' => $this->sender['name'],
|
||||||
|
@ -151,6 +160,10 @@ class libcalendaring_itip
|
||||||
$message->headers($headers, true);
|
$message->headers($headers, true);
|
||||||
$message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
|
$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
|
// finally send the message
|
||||||
$this->itip_send = true;
|
$this->itip_send = true;
|
||||||
$sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
|
$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);
|
array_unshift($reply_attendees, $replying_attendee);
|
||||||
$event['attendees'] = $reply_attendees;
|
$event['attendees'] = $reply_attendees;
|
||||||
}
|
}
|
||||||
|
if ($event['recurrence']) {
|
||||||
|
unset($event['recurrence']['EXCEPTIONS']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// set RSVP for every attendee
|
// set RSVP for every attendee
|
||||||
else if ($method == 'REQUEST') {
|
else if ($method == 'REQUEST') {
|
||||||
foreach ($event['attendees'] as $i => $attendee) {
|
foreach ($event['attendees'] as $i => $attendee) {
|
||||||
if ($attendee['status'] != 'DELEGATED') {
|
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
|
// compose multipart message using PEAR:Mail_Mime
|
||||||
$message = new Mail_mime("\r\n");
|
$message = new Mail_mime("\r\n");
|
||||||
|
@ -276,9 +297,10 @@ class libcalendaring_itip
|
||||||
* @param array Event object to delegate
|
* @param array Event object to delegate
|
||||||
* @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
|
* @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
|
||||||
* @param boolean The delegator's RSVP flag
|
* @param boolean The delegator's RSVP flag
|
||||||
|
* @param array List with indexes of new/updated attendees
|
||||||
* @return boolean True on success, False on failure
|
* @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)) {
|
if (is_string($delegate)) {
|
||||||
$delegates = rcube_mime::decode_address_list($delegate, 1, false);
|
$delegates = rcube_mime::decode_address_list($delegate, 1, false);
|
||||||
|
@ -324,6 +346,8 @@ class libcalendaring_itip
|
||||||
$delegate_attendee['delegated-from'] = $me['email'];
|
$delegate_attendee['delegated-from'] = $me['email'];
|
||||||
$event['attendees'][$delegate_index] = $delegate_attendee;
|
$event['attendees'][$delegate_index] = $delegate_attendee;
|
||||||
|
|
||||||
|
$attendees[] = $delegate_index;
|
||||||
|
|
||||||
$this->set_sender_email($me['email']);
|
$this->set_sender_email($me['email']);
|
||||||
return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
|
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
|
// check if the given itip object matches the last state
|
||||||
if ($existing) {
|
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']);
|
(!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;
|
$changed = is_object($event['changed']) ? $event['changed'] : $message_date;
|
||||||
$metadata = array(
|
$metadata = array(
|
||||||
'uid' => $event['uid'],
|
'uid' => $event['uid'],
|
||||||
|
'_instance' => $event['_instance'],
|
||||||
'changed' => $changed ? $changed->format('U') : 0,
|
'changed' => $changed ? $changed->format('U') : 0,
|
||||||
'sequence' => intval($event['sequence']),
|
'sequence' => intval($event['sequence']),
|
||||||
'method' => $method,
|
'method' => $method,
|
||||||
|
@ -580,12 +605,17 @@ class libcalendaring_itip
|
||||||
// for CANCEL messages, we can:
|
// for CANCEL messages, we can:
|
||||||
else if ($method == 'CANCEL') {
|
else if ($method == 'CANCEL') {
|
||||||
$title = $this->gettext('itipcancellation');
|
$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
|
// 1. remove the event from our calendar
|
||||||
$button_remove = html::tag('input', array(
|
$button_remove = html::tag('input', array(
|
||||||
'type' => 'button',
|
'type' => 'button',
|
||||||
'class' => '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'),
|
'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
|
// add localized texts for the delegation dialog
|
||||||
if (in_array('delegated', $actions)) {
|
if (in_array('delegated', $actions)) {
|
||||||
foreach (array('itipdelegated','itipcomment','delegateinvitation',
|
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,
|
return html::div($attrib,
|
||||||
html::div('label', $this->gettext('acceptinvitation')) .
|
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('label', $this->gettext('date'));
|
||||||
$table->add('date', Q($this->lib->event_date_text($event)));
|
$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('label', $this->gettext('recurring'));
|
||||||
$table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
|
$table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
|
||||||
}
|
}
|
||||||
|
|
|
@ -958,14 +958,48 @@ rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
|
||||||
return dialog;
|
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))) {
|
if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
|
||||||
rcmail.http_post(task + '/itip-remove',
|
rcmail.http_post(task + '/itip-remove',
|
||||||
{ uid: uid },
|
event,
|
||||||
rcmail.set_busy(true, 'itip.savingdata')
|
rcmail.set_busy(true, 'itip.savingdata')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1310,6 +1310,9 @@ class libcalendaring extends rcube_plugin
|
||||||
$charset = $part->ctype_parameters['charset'] ?: RCMAIL_CHARSET;
|
$charset = $part->ctype_parameters['charset'] ?: RCMAIL_CHARSET;
|
||||||
$this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $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
|
// stop on the part that has an iTip method specified
|
||||||
if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
|
if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
|
||||||
$this->mail_ical_parser->message_date = $this->ical_message->headers->date;
|
$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'] = preg_match(self::$email_regex, $headers->from, $m) ? $m[1] : '';
|
||||||
$object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
|
$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;
|
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 *********/
|
/********* Attendee handling functions *********/
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ class libvcalendar implements Iterator
|
||||||
{
|
{
|
||||||
private $timezone;
|
private $timezone;
|
||||||
private $attach_uri = null;
|
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 $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
|
||||||
private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE',
|
private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE',
|
||||||
'cutype' => 'CUTYPE', 'rsvp' => 'RSVP', 'delegated-from' => 'DELEGATED-FROM', 'delegated-to' => 'DELEGATED-TO');
|
'cutype' => 'CUTYPE', 'rsvp' => 'RSVP', 'delegated-from' => 'DELEGATED-FROM', 'delegated-to' => 'DELEGATED-TO');
|
||||||
|
@ -64,7 +64,7 @@ class libvcalendar implements Iterator
|
||||||
function __construct($tz = null)
|
function __construct($tz = null)
|
||||||
{
|
{
|
||||||
$this->timezone = $tz;
|
$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 'ATTENDEE':
|
||||||
case 'ORGANIZER':
|
case 'ORGANIZER':
|
||||||
$params = array();
|
$params = array('rsvp' => false);
|
||||||
foreach ($prop->parameters as $param) {
|
foreach ($prop->parameters as $param) {
|
||||||
switch ($param->name) {
|
switch ($param->name) {
|
||||||
case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
|
case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
|
||||||
|
@ -948,6 +948,13 @@ class libvcalendar implements Iterator
|
||||||
if (!empty($event['due']))
|
if (!empty($event['due']))
|
||||||
$ve->add($this->datetime_prop('DUE', $event['due'], false));
|
$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)
|
if ($recurrence_id)
|
||||||
$ve->add($recurrence_id);
|
$ve->add($recurrence_id);
|
||||||
|
|
||||||
|
@ -1081,7 +1088,7 @@ class libvcalendar implements Iterator
|
||||||
}
|
}
|
||||||
else if (!empty($attendee['email'])) {
|
else if (!empty($attendee['email'])) {
|
||||||
if (isset($attendee['rsvp']))
|
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)));
|
$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['acceptattendee'] = 'Accept participant';
|
||||||
$labels['declineattendee'] = 'Decline participant';
|
$labels['declineattendee'] = 'Decline participant';
|
||||||
$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
|
$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['youhaveaccepted'] = 'You have accepted this invitation';
|
||||||
$labels['youhavetentative'] = 'You have tentatively 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
|
// 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
|
// 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
|
// 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
|
// 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_end->add($this->duration);
|
||||||
|
|
||||||
$next = $this->object->to_array();
|
$next = $this->object->to_array();
|
||||||
$next['recurrence_id'] = $next_start->format('Y-m-d');
|
|
||||||
$next['start'] = $next_start;
|
$next['start'] = $next_start;
|
||||||
$next['end'] = $next_end;
|
$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']);
|
unset($next['_formatobj']);
|
||||||
|
|
||||||
return $next;
|
return $next;
|
||||||
|
|
|
@ -26,7 +26,7 @@ class kolab_format_event extends kolab_format_xcal
|
||||||
{
|
{
|
||||||
public $CTYPEv2 = 'application/x-vnd.kolab.event';
|
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 $objclass = 'Event';
|
||||||
protected $read_func = 'readEvent';
|
protected $read_func = 'readEvent';
|
||||||
|
@ -44,6 +44,9 @@ class kolab_format_event extends kolab_format_xcal
|
||||||
$this->obj = $data;
|
$this->obj = $data;
|
||||||
$this->loaded = true;
|
$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);
|
$vexceptions->push($exevent->obj);
|
||||||
|
|
||||||
// write cleaned-up exception data back to memory/cache
|
// 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);
|
$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
|
// cache this data
|
||||||
$this->data = $object;
|
$this->data = $object;
|
||||||
|
@ -220,15 +226,16 @@ class kolab_format_event extends kolab_format_xcal
|
||||||
*
|
*
|
||||||
* @return array List of tags to save in cache
|
* @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);
|
$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
|
// preserve this property for date serialization
|
||||||
$exception['allday'] = $master['allday'];
|
$exception['allday'] = $master['allday'];
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,16 @@ class kolab_format_task extends kolab_format_xcal
|
||||||
protected $read_func = 'readTodo';
|
protected $read_func = 'readTodo';
|
||||||
protected $write_func = 'writeTodo';
|
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
|
* 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
|
* @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';
|
$tags[] = 'x-complete';
|
||||||
|
|
||||||
if ($this->data['priority'] == 1)
|
if ($object['priority'] == 1)
|
||||||
$tags[] = 'x-flagged';
|
$tags[] = 'x-flagged';
|
||||||
|
|
||||||
if ($this->data['parent_id'])
|
if ($object['parent_id'])
|
||||||
$tags[] = 'x-parent:' . $this->data['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');
|
public static $scheduling_properties = array('start', 'end', 'location');
|
||||||
|
|
||||||
|
protected $_scheduling_properties = null;
|
||||||
|
|
||||||
protected $sensitivity_map = array(
|
protected $sensitivity_map = array(
|
||||||
'public' => kolabformat::ClassPublic,
|
'public' => kolabformat::ClassPublic,
|
||||||
'private' => kolabformat::ClassPrivate,
|
'private' => kolabformat::ClassPrivate,
|
||||||
|
@ -317,21 +319,11 @@ abstract class kolab_format_xcal extends kolab_format
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$object['sequence'] = $old_sequence;
|
$object['sequence'] = $old_sequence;
|
||||||
$old = $this->data['uid'] ? $this->data : $this->to_array();
|
|
||||||
|
|
||||||
// increment sequence when updating properties relevant for scheduling.
|
// 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."
|
// RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
|
||||||
foreach (self::$scheduling_properties as $prop) {
|
if ($this->check_rescheduling($object)) {
|
||||||
$a = $old[$prop];
|
|
||||||
$b = $object[$prop];
|
|
||||||
if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
|
|
||||||
$a = $a->format('Y-m-d');
|
|
||||||
$b = $b->format('Y-m-d');
|
|
||||||
}
|
|
||||||
if ($a != $b) {
|
|
||||||
$object['sequence']++;
|
$object['sequence']++;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,7 +357,7 @@ abstract class kolab_format_xcal extends kolab_format
|
||||||
|
|
||||||
// set attendee RSVP if missing
|
// set attendee RSVP if missing
|
||||||
if (!isset($attendee['rsvp'])) {
|
if (!isset($attendee['rsvp'])) {
|
||||||
$object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = true;
|
$object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = $reschedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
$att = new Attendee;
|
$att = new Attendee;
|
||||||
|
@ -619,22 +611,68 @@ abstract class kolab_format_xcal extends kolab_format
|
||||||
*
|
*
|
||||||
* @return array List of tags to save in cache
|
* @return array List of tags to save in cache
|
||||||
*/
|
*/
|
||||||
public function get_tags()
|
public function get_tags($obj = null)
|
||||||
{
|
{
|
||||||
$tags = array();
|
$tags = array();
|
||||||
|
$object = $obj ?: $this->data;
|
||||||
|
|
||||||
if (!empty($this->data['valarms'])) {
|
if (!empty($object['valarms'])) {
|
||||||
$tags[] = 'x-has-alarms';
|
$tags[] = 'x-has-alarms';
|
||||||
}
|
}
|
||||||
|
|
||||||
// create tags reflecting participant status
|
// create tags reflecting participant status
|
||||||
if (is_array($this->data['attendees'])) {
|
if (is_array($object['attendees'])) {
|
||||||
foreach ($this->data['attendees'] as $attendee) {
|
foreach ($object['attendees'] as $attendee) {
|
||||||
if (!empty($attendee['email']) && !empty($attendee['status']))
|
if (!empty($attendee['email']) && !empty($attendee['status']))
|
||||||
$tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($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;
|
$type = $this->type;
|
||||||
|
|
||||||
// copy attachments from old message
|
// 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) {
|
foreach ((array)$old['_attachments'] as $key => $att) {
|
||||||
if (!isset($object['_attachments'][$key])) {
|
if (!isset($object['_attachments'][$key])) {
|
||||||
$object['_attachments'][$key] = $old['_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
|
// 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']) {
|
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
|
||||||
if (!isset($object['photo']))
|
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]);
|
unset($object['_attachments'][$key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1010,7 +1011,7 @@ class kolab_storage_folder extends kolab_storage_folder_api
|
||||||
foreach ((array)$object['_attachments'] as $key => $att) {
|
foreach ((array)$object['_attachments'] as $key => $att) {
|
||||||
if (empty($att['content']) && !empty($att['id'])) {
|
if (empty($att['content']) && !empty($att['id'])) {
|
||||||
// @TODO: use IMAP CATENATE to skip attachment fetch+push operation
|
// @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) {
|
if ($is_file) {
|
||||||
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
|
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
|
||||||
if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
|
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