Split recurring event into a new series when modifying with this-and-future option (#4386); optimize copying of attachments into new event
This commit is contained in:
parent
5e176baa08
commit
02ef2e6050
7 changed files with 233 additions and 88 deletions
|
@ -865,6 +865,7 @@ class calendar extends rcube_plugin
|
|||
$event['id'] = $event['uid'];
|
||||
$event['_savemode'] = 'all';
|
||||
$this->cleanup_event($event);
|
||||
$this->event_save_success($event, null, $action, true);
|
||||
}
|
||||
$reload = $success && $event['recurrence'] ? 2 : 1;
|
||||
break;
|
||||
|
@ -873,21 +874,15 @@ class calendar extends rcube_plugin
|
|||
$this->write_preprocess($event, $action);
|
||||
if ($success = $this->driver->edit_event($event)) {
|
||||
$this->cleanup_event($event);
|
||||
if ($success !== true) {
|
||||
$event['id'] = $success;
|
||||
$old = null;
|
||||
}
|
||||
$this->event_save_success($event, $old, $action, $success);
|
||||
}
|
||||
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
|
||||
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
|
||||
break;
|
||||
|
||||
case "resize":
|
||||
$this->write_preprocess($event, $action);
|
||||
if ($success = $this->driver->resize_event($event)) {
|
||||
if ($success !== true) {
|
||||
$event['id'] = $success;
|
||||
$old = null;
|
||||
}
|
||||
$this->event_save_success($event, $old, $action, $success);
|
||||
}
|
||||
$reload = $event['_savemode'] ? 2 : 1;
|
||||
break;
|
||||
|
@ -895,10 +890,7 @@ class calendar extends rcube_plugin
|
|||
case "move":
|
||||
$this->write_preprocess($event, $action);
|
||||
if ($success = $this->driver->move_event($event)) {
|
||||
if ($success !== true) {
|
||||
$event['id'] = $success;
|
||||
$old = null;
|
||||
}
|
||||
$this->event_save_success($event, $old, $action, $success);
|
||||
}
|
||||
$reload = $success && $event['_savemode'] ? 2 : 1;
|
||||
break;
|
||||
|
@ -958,6 +950,9 @@ class calendar extends rcube_plugin
|
|||
else
|
||||
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
|
||||
}
|
||||
else if ($success) {
|
||||
$this->event_save_success($event, $old, $action, $success);
|
||||
}
|
||||
break;
|
||||
|
||||
case "undo":
|
||||
|
@ -1000,8 +995,8 @@ class calendar extends rcube_plugin
|
|||
$event = $ev;
|
||||
|
||||
// compose a list of attendees affected by this change
|
||||
$updated_attendees = array_filter(array_map(function($j) use ($ev) {
|
||||
return $ev['attendees'][$j];
|
||||
$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)) {
|
||||
|
@ -1147,35 +1142,6 @@ class calendar extends rcube_plugin
|
|||
$this->rc->output->show_message('calendar.errorsaving', 'error');
|
||||
}
|
||||
|
||||
// send out notifications
|
||||
if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) {
|
||||
$_savemode = $event['_savemode'];
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
// send notification for the main event when savemode is 'all'
|
||||
if ($_savemode == 'all' && $event['recurrence_id']) {
|
||||
$event['id'] = $event['recurrence_id'];
|
||||
$event = $this->driver->get_event($event);
|
||||
unset($event['_instance'], $event['recurrence_date']);
|
||||
}
|
||||
|
||||
// only notify if data really changed (TODO: do diff check on client already)
|
||||
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
|
||||
$sent = $this->notify_attendees($event, $old, $action, $event['_comment']);
|
||||
if ($sent > 0)
|
||||
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
|
||||
else if ($sent < 0)
|
||||
$this->rc->output->show_message('calendar.errornotifying', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// unlock client
|
||||
$this->rc->output->command('plugin.unlock_saving');
|
||||
|
||||
|
@ -1190,6 +1156,61 @@ 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['id'] = $success;
|
||||
$event['_savemode'] = 'all';
|
||||
$event['attendees'] = $master['attendees']; // this tricks us into the next if clause
|
||||
$old = null;
|
||||
}
|
||||
|
||||
// send out notifications
|
||||
if ($event['_notify'] && ($event['attendees'] || $old['attendees'])) {
|
||||
$_savemode = $event['_savemode'];
|
||||
|
||||
// send notification for the main event when savemode is 'all'
|
||||
if ($action != 'remove' && $_savemode == 'all' && $old['recurrence_id']) {
|
||||
$event['id'] = $old['recurrence_id'];
|
||||
$event = $this->driver->get_event($event);
|
||||
unset($event['_instance'], $event['recurrence_date']);
|
||||
}
|
||||
else {
|
||||
// make sure we have the complete record
|
||||
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
|
||||
}
|
||||
|
||||
$event['_savemode'] = $_savemode;
|
||||
|
||||
if ($old) {
|
||||
$old['thisandfuture'] = $_savemode == 'future';
|
||||
}
|
||||
|
||||
// only notify if data really changed (TODO: do diff check on client already)
|
||||
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
|
||||
$sent = $this->notify_attendees($event, $old, $action, $event['_comment']);
|
||||
if ($sent > 0)
|
||||
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
|
||||
else if ($sent < 0)
|
||||
$this->rc->output->show_message('calendar.errornotifying', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for load-requests from fullcalendar
|
||||
* This will return pure JSON formatted output
|
||||
|
|
|
@ -2432,6 +2432,10 @@ function rcube_calendar_ui(settings)
|
|||
|
||||
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)
|
||||
if (data.status == 'DECLINED' || data.role == 'NON-PARTICIPANT') {
|
||||
|
|
|
@ -639,6 +639,7 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
if (($next_event['start'] <= $end && $next_event['end'] >= $start) || ($event_id && $rec_id == $event_id)) {
|
||||
$rec_event = $this->_to_rcube_event($next_event);
|
||||
$rec_event['_instance'] = $instance_id;
|
||||
$rec_event['_count'] = $i + 1;
|
||||
|
||||
if ($overlay_data || $exdata[$datestr]) // copy data from exception
|
||||
kolab_driver::merge_exception_data($rec_event, $exdata[$datestr] ?: $overlay_data);
|
||||
|
@ -687,38 +688,7 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
*/
|
||||
private function _from_rcube_event($event, $old = array())
|
||||
{
|
||||
// in kolab_storage attachments are indexed by content-id
|
||||
$event['_attachments'] = array();
|
||||
if (is_array($event['attachments'])) {
|
||||
foreach ($event['attachments'] as $attachment) {
|
||||
$key = null;
|
||||
// Roundcube ID has nothing to do with the storage ID, remove it
|
||||
if ($attachment['content'] || $attachment['path']) {
|
||||
unset($attachment['id']);
|
||||
}
|
||||
else {
|
||||
foreach ((array)$old['_attachments'] as $cid => $oldatt) {
|
||||
if ($attachment['id'] == $oldatt['id'])
|
||||
$key = $cid;
|
||||
}
|
||||
}
|
||||
|
||||
// flagged for deletion => set to false
|
||||
if ($attachment['_deleted']) {
|
||||
$event['_attachments'][$key] = false;
|
||||
}
|
||||
// replace existing entry
|
||||
else if ($key) {
|
||||
$event['_attachments'][$key] = $attachment;
|
||||
}
|
||||
// append as new attachment
|
||||
else {
|
||||
$event['_attachments'][] = $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
unset($event['attachments']);
|
||||
}
|
||||
$event = kolab_driver::from_rcube_event($event, $old);
|
||||
|
||||
// set current user as ORGANIZER
|
||||
$identity = $this->cal->rc->user->list_emails(true);
|
||||
|
|
|
@ -621,7 +621,19 @@ class kolab_driver extends calendar_driver
|
|||
*/
|
||||
public function edit_rsvp(&$event, $status, $attendees)
|
||||
{
|
||||
if (($ret = $this->update_attendees($event, $attendees)) && $this->rc->config->get('kolab_invitation_calendars')) {
|
||||
$update_event = $event;
|
||||
|
||||
// apply changes to master (and all exceptions)
|
||||
if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
|
||||
if ($storage = $this->get_calendar($event['calendar'])) {
|
||||
$update_event = $storage->get_event($event['recurrence_id']);
|
||||
$update_event['_savemode'] = $event['_savemode'];
|
||||
unset($update_event['recurrence_id']);
|
||||
self::merge_attendee_data($update_event, $attendees);
|
||||
}
|
||||
}
|
||||
|
||||
if (($ret = $this->update_attendees($update_event, $attendees)) && $this->rc->config->get('kolab_invitation_calendars')) {
|
||||
// re-assign to the according (virtual) calendar
|
||||
if (strtoupper($status) == 'DECLINED')
|
||||
$event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
|
||||
|
@ -687,6 +699,7 @@ class kolab_driver extends calendar_driver
|
|||
{
|
||||
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
|
||||
unset($ev['sequence']);
|
||||
self::clear_attandee_noreply($ev);
|
||||
return $this->update_event($event + $ev);
|
||||
}
|
||||
|
||||
|
@ -703,6 +716,7 @@ class kolab_driver extends calendar_driver
|
|||
{
|
||||
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
|
||||
unset($ev['sequence']);
|
||||
self::clear_attandee_noreply($ev);
|
||||
return $this->update_event($event + $ev);
|
||||
}
|
||||
|
||||
|
@ -928,6 +942,7 @@ class kolab_driver extends calendar_driver
|
|||
if ($old['recurrence'] || $old['recurrence_id']) {
|
||||
$master = $old['recurrence_id'] ? $fromcalendar->get_event($old['recurrence_id']) : $old;
|
||||
$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')
|
||||
|
@ -953,20 +968,70 @@ class kolab_driver extends calendar_driver
|
|||
case 'new':
|
||||
// save submitted data as new (non-recurring) event
|
||||
$event['recurrence'] = array();
|
||||
$event['_copyfrom'] = $object['_msguid'];
|
||||
$event['_mailbox'] = $object['_mailbox'];
|
||||
$event['uid'] = $this->cal->generate_uid();
|
||||
unset($event['recurrence_id'], $event['_instance'], $event['id']);
|
||||
unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
|
||||
|
||||
// copy attachment data to new event
|
||||
foreach ((array)$event['attachments'] as $idx => $attachment) {
|
||||
if (!$attachment['content'])
|
||||
$event['attachments'][$idx]['content'] = $this->get_attachment_body($attachment['id'], $master);
|
||||
}
|
||||
// copy attachment metadata to new event
|
||||
$event = self::from_rcube_event($event, $object);
|
||||
|
||||
self::clear_attandee_noreply($event);
|
||||
if ($success = $storage->insert_event($event))
|
||||
$success = $event['uid'];
|
||||
break;
|
||||
|
||||
case 'future':
|
||||
// create a new recurring event
|
||||
$event['_copyfrom'] = $object['_msguid'];
|
||||
$event['_mailbox'] = $object['_mailbox'];
|
||||
$event['uid'] = $this->cal->generate_uid();
|
||||
unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
|
||||
|
||||
// copy attachment metadata to new event
|
||||
$event = self::from_rcube_event($event, $object);
|
||||
|
||||
// remove recurrence exceptions on re-scheduling
|
||||
if ($reschedule) {
|
||||
unset($event['recurrence']['EXCEPTIONS']);
|
||||
}
|
||||
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'];
|
||||
});
|
||||
}
|
||||
|
||||
// compute remaining occurrences
|
||||
if ($event['recurrence']['COUNT']) {
|
||||
if (!$old['_count'])
|
||||
$old['_count'] = $this->get_recurrence_count($object, $event['start']);
|
||||
$event['recurrence']['COUNT'] -= intval($old['_count']);
|
||||
}
|
||||
|
||||
// set until-date on master event
|
||||
$master['recurrence']['UNTIL'] = clone $event['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'];
|
||||
});
|
||||
}
|
||||
|
||||
// save new event
|
||||
if ($success = $storage->insert_event($event)) {
|
||||
$success = $event['uid'];
|
||||
|
||||
// update master event (no rescheduling!)
|
||||
$master['sequence'] = $object['sequence'];
|
||||
self::clear_attandee_noreply($master);
|
||||
$udated = $storage->update_event($master);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'current':
|
||||
// recurring instances shall not store recurrence rules and attachments
|
||||
$event['recurrence'] = array();
|
||||
|
@ -1214,6 +1279,17 @@ class kolab_driver extends calendar_driver
|
|||
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
|
||||
*
|
||||
|
@ -1570,6 +1646,29 @@ class kolab_driver extends calendar_driver
|
|||
return $storage->get_recurring_events($event, $start, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function get_recurrence_count($event, $dtstart)
|
||||
{
|
||||
// use libkolab to compute recurring events
|
||||
if (class_exists('kolabcalendaring') && $object['_formatobj']) {
|
||||
$recurrence = new kolab_date_recurrence($object['_formatobj']);
|
||||
}
|
||||
else {
|
||||
// fallback to local recurrence implementation
|
||||
require_once($this->cal->home . '/lib/calendar_recurrence.php');
|
||||
$recurrence = new calendar_recurrence($this->cal, $event);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch free/busy information from a person within the given range
|
||||
*/
|
||||
|
@ -1749,12 +1848,62 @@ class kolab_driver extends calendar_driver
|
|||
$record['_instance'] = $record['start']->format($recurrence_id_format);
|
||||
}
|
||||
|
||||
// clean up exception data
|
||||
if (is_array($record['recurrence']['EXCEPTIONS'])) {
|
||||
array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
|
||||
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
|
||||
});
|
||||
}
|
||||
|
||||
// remove internals
|
||||
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public static function from_rcube_event($event, $old = array())
|
||||
{
|
||||
// in kolab_storage attachments are indexed by content-id
|
||||
if (is_array($event['attachments'])) {
|
||||
$event['_attachments'] = array();
|
||||
|
||||
foreach ($event['attachments'] as $attachment) {
|
||||
$key = null;
|
||||
// Roundcube ID has nothing to do with the storage ID, remove it
|
||||
if ($attachment['content'] || $attachment['path']) {
|
||||
unset($attachment['id']);
|
||||
}
|
||||
else {
|
||||
foreach ((array)$old['_attachments'] as $cid => $oldatt) {
|
||||
if ($attachment['id'] == $oldatt['id'])
|
||||
$key = $cid;
|
||||
}
|
||||
}
|
||||
|
||||
// flagged for deletion => set to false
|
||||
if ($attachment['_deleted']) {
|
||||
$event['_attachments'][$key] = false;
|
||||
}
|
||||
// replace existing entry
|
||||
else if ($key) {
|
||||
$event['_attachments'][$key] = $attachment;
|
||||
}
|
||||
// append as new attachment
|
||||
else {
|
||||
$event['_attachments'][] = $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
unset($event['attachments']);
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set CSS class according to the event's attendde partstat
|
||||
*/
|
||||
|
|
|
@ -250,7 +250,7 @@ class libcalendaring_itip
|
|||
// set RSVP for every attendee
|
||||
else if ($method == 'REQUEST') {
|
||||
foreach ($event['attendees'] as $i => $attendee) {
|
||||
if ($attendee['status'] != 'DELEGATED' && !isset($attendee['rsvp'])) {
|
||||
if ($attendee['status'] != 'DELEGATED') {
|
||||
$event['attendees'][$i]['rsvp']= (bool)$rsvp;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -965,7 +965,7 @@ rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback)
|
|||
{
|
||||
var mnu = $('<ul></ul>').addClass('popupmenu libcal-rsvp-replymode');
|
||||
|
||||
$.each(['all','current','future'], function(i, mode) {
|
||||
$.each(['all','current'/*,'future'*/], function(i, mode) {
|
||||
$('<li><a>' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '</a>')
|
||||
.addClass('ui-menu-item')
|
||||
.attr('rel', mode)
|
||||
|
|
|
@ -616,7 +616,8 @@ class kolab_storage_folder extends kolab_storage_folder_api
|
|||
$type = $this->type;
|
||||
|
||||
// copy attachments from old message
|
||||
if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) {
|
||||
$copyfrom = $object['_copyfrom'] ?: $object['_msguid'];
|
||||
if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) {
|
||||
foreach ((array)$old['_attachments'] as $key => $att) {
|
||||
if (!isset($object['_attachments'][$key])) {
|
||||
$object['_attachments'][$key] = $old['_attachments'][$key];
|
||||
|
@ -628,7 +629,7 @@ class kolab_storage_folder extends kolab_storage_folder_api
|
|||
// load photo.attachment from old Kolab2 format to be directly embedded in xcard block
|
||||
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
|
||||
if (!isset($object['photo']))
|
||||
$object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
|
||||
$object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']);
|
||||
unset($object['_attachments'][$key]);
|
||||
}
|
||||
}
|
||||
|
@ -1010,7 +1011,7 @@ class kolab_storage_folder extends kolab_storage_folder_api
|
|||
foreach ((array)$object['_attachments'] as $key => $att) {
|
||||
if (empty($att['content']) && !empty($att['id'])) {
|
||||
// @TODO: use IMAP CATENATE to skip attachment fetch+push operation
|
||||
$msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
|
||||
$msguid = $object['_copyfrom'] ?: ($object['_msguid'] ?: $object['uid']);
|
||||
if ($is_file) {
|
||||
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
|
||||
if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
|
||||
|
|
Loading…
Add table
Reference in a new issue