diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index f5c2c043..8be59564 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -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 diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index e7f55cd0..5a26c188 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -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') { diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index a392a9f8..64bb0826 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -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); diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 48dbafd0..d6fb55ac 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -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 */ diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php index 41148c60..14dacf4d 100644 --- a/plugins/libcalendaring/lib/libcalendaring_itip.php +++ b/plugins/libcalendaring/lib/libcalendaring_itip.php @@ -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; } } diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js index 71c101da..1c8fe16f 100644 --- a/plugins/libcalendaring/libcalendaring.js +++ b/plugins/libcalendaring/libcalendaring.js @@ -965,7 +965,7 @@ rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback) { var mnu = $('').addClass('popupmenu libcal-rsvp-replymode'); - $.each(['all','current','future'], function(i, mode) { + $.each(['all','current'/*,'future'*/], function(i, mode) { $('
  • ' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '') .addClass('ui-menu-item') .attr('rel', mode) diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php index ab3c63f8..e0bf52e3 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -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)) {