33 commits - plugins/calendar plugins/libcalendaring plugins/libkolab
Thomas Brüderli
bruederli at kolabsys.com
Fri Feb 20 11:46:10 CET 2015
plugins/calendar/calendar.php | 229 +++++++--
plugins/calendar/calendar_ui.js | 105 +++-
plugins/calendar/drivers/calendar_driver.php | 24 -
plugins/calendar/drivers/kolab/kolab_calendar.php | 185 ++-----
plugins/calendar/drivers/kolab/kolab_driver.php | 494 ++++++++++++++++++---
plugins/calendar/lib/calendar_recurrence.php | 5
plugins/calendar/print.js | 3
plugins/calendar/skins/larry/calendar.css | 17
plugins/calendar/skins/larry/print.css | 22
plugins/calendar/skins/larry/templates/print.html | 1
plugins/libcalendaring/lib/libcalendaring_itip.php | 64 ++
plugins/libcalendaring/libcalendaring.js | 38 +
plugins/libcalendaring/libcalendaring.php | 34 +
plugins/libcalendaring/libvcalendar.php | 15
plugins/libcalendaring/localization/en_US.inc | 8
plugins/libkolab/config.inc.php.dist | 2
plugins/libkolab/lib/kolab_date_recurrence.php | 6
plugins/libkolab/lib/kolab_format_event.php | 25 -
plugins/libkolab/lib/kolab_format_task.php | 26 -
plugins/libkolab/lib/kolab_format_xcal.php | 74 ++-
plugins/libkolab/lib/kolab_storage_folder.php | 7
21 files changed, 1059 insertions(+), 325 deletions(-)
New commits:
commit 52bbf63a8e0c6a785facc395206d1a1f11ee7d05
Merge: 5966cff f972f4a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri Feb 20 10:18:59 2015 +0100
Merge branch 'dev/recurring-invitations'
commit f972f4a511a7c3710ec5f5cee001133837797923
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri Feb 20 10:18:21 2015 +0100
Disable the 'future' savemode for event deletion if attendees are involved
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 5a26c18..eb57fa7 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2603,25 +2603,19 @@ function rcube_calendar_ui(settings)
// recurring event: user needs to select the savemode
if (event.recurrence) {
- var disabled_state = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
-/*
- if (_has_attendees) {
- if (action == 'remove') {
- if (!_is_organizer) {
- message_label = 'removerecurringallonly';
- disabled_state = ' disabled';
- }
- }
- else if (is_organizer(event)) {
- disabled_state = ' disabled';
- }
+ var future_disabled = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
+
+ // disable the 'future' savemode if attendees are involved
+ // reason: no calendaring system supports the thisandfuture range parameter
+ if (action == 'remove' && _has_attendees && is_organizer(event)) {
+ future_disabled = ' disabled';
}
-*/
+
html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
rcmail.gettext(message_label, 'calendar') + '</div>' +
'<div class="savemode">' +
- '<a href="#current" class="button' + disabled_state + '">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
- '<a href="#future" class="button' + disabled_state + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
+ '<a href="#current" class="button">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
+ '<a href="#future" class="button' + future_disabled + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
'<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
(action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
'</div>';
commit 515a7d9ef6dcdc1c1de2488745a511bdac256896
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri Feb 20 09:25:24 2015 +0100
Small fixes to recurring event invitations (#4387)
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 8be5956..f6079db 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1171,11 +1171,12 @@ class calendar extends rcube_plugin
$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';
- $event['attendees'] = $master['attendees']; // this tricks us into the next if clause
$old = null;
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index d6fb55a..88c2a38 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -993,24 +993,37 @@ class kolab_driver extends calendar_driver
// remove recurrence exceptions on re-scheduling
if ($reschedule) {
- unset($event['recurrence']['EXCEPTIONS']);
+ 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, $event['start']);
+ $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 $event['start'];
+ $master['recurrence']['UNTIL'] = clone $old['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']);
@@ -1020,6 +1033,11 @@ class kolab_driver extends calendar_driver
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)) {
@@ -1115,7 +1133,7 @@ class kolab_driver extends calendar_driver
// when saving an instance in 'all' mode, copy recurrence exceptions over
if ($old['recurrence_id']) {
- $event['recurrence'] = $master['recurrence'];
+ $event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS'];
}
// TODO: forward changes to exceptions (which do not yet have differing values stored)
commit 02ef2e60505a6319599042c9e6e92a7a2f42de8c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri Feb 20 00:11:40 2015 +0100
Split recurring event into a new series when modifying with this-and-future option (#4386); optimize copying of attachments into new event
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index f5c2c04..8be5956 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,24 +1142,63 @@ 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'];
+ // unlock client
+ $this->rc->output->command('plugin.unlock_saving');
- // make sure we have the complete record
- $event = $action == 'remove' ? $old : $this->driver->get_event($event);
- $event['_savemode'] = $_savemode;
+ // update event object on the client or trigger a complete refretch if too complicated
+ if ($reload) {
+ $args = array('source' => $event['calendar']);
+ if ($reload > 1)
+ $args['refetch'] = true;
+ else if ($success && $action != 'remove')
+ $args['update'] = $this->_client_event($this->driver->get_event($event), true);
+ $this->rc->output->command('plugin.refresh_calendar', $args);
+ }
+ }
- if ($old) {
- $old['thisandfuture'] = $_savemode == 'future';
+ /**
+ * 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 ($_savemode == 'all' && $event['recurrence_id']) {
- $event['id'] = $event['recurrence_id'];
+ 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)) {
@@ -1175,19 +1209,6 @@ class calendar extends rcube_plugin
$this->rc->output->show_message('calendar.errornotifying', 'error');
}
}
-
- // unlock client
- $this->rc->output->command('plugin.unlock_saving');
-
- // update event object on the client or trigger a complete refretch if too complicated
- if ($reload) {
- $args = array('source' => $event['calendar']);
- if ($reload > 1)
- $args['refetch'] = true;
- else if ($success && $action != 'remove')
- $args['update'] = $this->_client_event($this->driver->get_event($event), true);
- $this->rc->output->command('plugin.refresh_calendar', $args);
- }
}
/**
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index e7f55cd..5a26c18 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 a392a9f..64bb082 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 48dbafd..d6fb55a 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();
@@ -1215,6 +1280,17 @@ class kolab_driver extends calendar_driver
}
/**
+ * 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
@@ -1571,6 +1647,29 @@ class kolab_driver extends calendar_driver
}
/**
+ *
+ */
+ 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
*/
public function get_freebusy_list($email, $start, $end)
@@ -1749,6 +1848,13 @@ 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']);
@@ -1756,6 +1862,49 @@ class kolab_driver extends calendar_driver
}
/**
+ *
+ */
+ 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
*/
public static function add_partstat_class($event, $partstats, $user = null)
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 41148c6..14dacf4 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 71c101d..1c8fe16 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 = $('<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)
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index ab3c63f..e0bf52e 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)) {
commit 5e176baa0832da051f50ef9740fda7c22cb4702f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Feb 19 18:09:12 2015 +0100
Pass a list of updated attendess to the backend driver on RSVP reply from calendar view
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index fc4bbff..f5c2c04 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -978,7 +978,8 @@ class calendar extends rcube_plugin
case "rsvp":
$itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
- $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC);
+ $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST);
+ $attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST);
$reply_comment = $event['comment'];
$this->write_preprocess($event, 'edit');
@@ -990,7 +991,7 @@ class calendar extends rcube_plugin
// send invitation to delegatee + add it as attendee
if ($status == 'delegated' && $event['to']) {
$itip = $this->load_itip();
- if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'])) {
+ if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'], $attendees)) {
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
$noreply = false;
}
@@ -998,7 +999,12 @@ class calendar extends rcube_plugin
$event = $ev;
- if ($success = $this->driver->edit_rsvp($event, $status)) {
+ // compose a list of attendees affected by this change
+ $updated_attendees = array_filter(array_map(function($j) use ($ev) {
+ return $ev['attendees'][$j];
+ }, $attendees));
+
+ if ($success = $this->driver->edit_rsvp($event, $status, $updated_attendees)) {
$noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);
$noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
$reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1;
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index c9334ec..e7f55cd 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2411,6 +2411,7 @@ function rcube_calendar_ui(settings)
}
// update attendee status
+ attendees = [];
for (var data, i=0; i < me.selected_event.attendees.length; i++) {
data = me.selected_event.attendees[i];
if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) {
@@ -2419,6 +2420,7 @@ function rcube_calendar_ui(settings)
if (data.status == 'DELEGATED') {
data['delegated-to'] = delegate.to;
+ data.rsvp = delegate.rsvp
}
else {
if (data['delegated-to']) {
@@ -2427,6 +2429,8 @@ function rcube_calendar_ui(settings)
data.role = 'REQ-PARTICIPANT';
}
}
+
+ attendees.push(i)
}
// set free_busy status to transparent if declined (#4425)
@@ -2459,11 +2463,11 @@ function rcube_calendar_ui(settings)
});
}
else if (settings.invitation_calendars) {
- update_event('rsvp', submit_data, { status:response, noreply:noreply });
+ update_event('rsvp', submit_data, { status:response, noreply:noreply, attendees:attendees });
}
else {
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
- rcmail.http_post('event', { action:'rsvp', e:submit_data, status:response, noreply:noreply });
+ rcmail.http_post('event', { action:'rsvp', e:submit_data, status:response, attendees:attendees, noreply:noreply });
}
event_show_dialog(me.selected_event);
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index aecd2e1..659f4a0 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -197,9 +197,10 @@ abstract class calendar_driver
*
* @param array Hash array with event properties
* @param string New participant status
+ * @param array List of hash arrays with updated attendees
* @return boolean True on success, False on error
*/
- public function edit_rsvp(&$event, $status)
+ public function edit_rsvp(&$event, $status, $attendees)
{
return $this->edit_event($event);
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 5e6f3b3..48dbafd 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -619,9 +619,9 @@ class kolab_driver extends calendar_driver
* @param string New participant status
* @return boolean True on success, False on error
*/
- public function edit_rsvp(&$event, $status)
+ public function edit_rsvp(&$event, $status, $attendees)
{
- if (($ret = $this->update_event($event)) && $this->rc->config->get('kolab_invitation_calendars')) {
+ if (($ret = $this->update_attendees($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;
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index e7de5c8..41148c6 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -297,9 +297,10 @@ class libcalendaring_itip
* @param array Event object to delegate
* @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
* @param boolean The delegator's RSVP flag
+ * @param array List with indexes of new/updated attendees
* @return boolean True on success, False on failure
*/
- public function delegate_to(&$event, $delegate, $rsvp = false)
+ public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array())
{
if (is_string($delegate)) {
$delegates = rcube_mime::decode_address_list($delegate, 1, false);
@@ -345,6 +346,8 @@ class libcalendaring_itip
$delegate_attendee['delegated-from'] = $me['email'];
$event['attendees'][$delegate_index] = $delegate_attendee;
+ $attendees[] = $delegate_index;
+
$this->set_sender_email($me['email']);
return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
}
commit 026d62d2351c58b5e28777ccc8767415490b4985
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Feb 19 15:58:32 2015 +0100
Avoid comparison errors if recurrence is set to '' (used to unset recurrence rules)
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index e6507d6..4d3a758 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -661,7 +661,7 @@ abstract class kolab_format_xcal extends kolab_format
$a = $a->format('Y-m-d');
$b = $b->format('Y-m-d');
}
- if ($prop == 'recurrence') {
+ if ($prop == 'recurrence' && is_array($a) && is_array($b)) {
unset($a['EXCEPTIONS']);
unset($b['EXCEPTIONS']);
$a = array_filter($a);
commit db637619c3cfa5fc73e55c7f60adba9c2c635e0a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Feb 19 15:57:37 2015 +0100
Omit RSVP flag in iCal export if not true
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 07612d5..826e8d8 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -1088,7 +1088,7 @@ class libvcalendar implements Iterator
}
else if (!empty($attendee['email'])) {
if (isset($attendee['rsvp']))
- $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : 'FALSE';
+ $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
$ve->add('ATTENDEE', 'mailto:' . $attendee['email'], array_filter(self::map_keys($attendee, $this->attendee_keymap)));
}
}
commit 61037eb97c8580d385fc625db064f3855d8a823f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Feb 19 15:56:46 2015 +0100
Fix RSVP flag in iTip REQUESTS
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index b4a7aee..e7de5c8 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -250,8 +250,8 @@ class libcalendaring_itip
// set RSVP for every attendee
else if ($method == 'REQUEST') {
foreach ($event['attendees'] as $i => $attendee) {
- if ($attendee['status'] != 'DELEGATED') {
- $event['attendees'][$i]['rsvp']= $rsvp ? true : null;
+ if ($attendee['status'] != 'DELEGATED' && !isset($attendee['rsvp'])) {
+ $event['attendees'][$i]['rsvp']= (bool)$rsvp;
}
}
}
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 0c90e85..91efb26 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -118,7 +118,7 @@ class kolab_format_event extends kolab_format_xcal
$vexceptions->push($exevent->obj);
// write cleaned-up exception data back to memory/cache
- $object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($compacted, $object);
+ $object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($exevent->data, $object);
}
$this->obj->setExceptions($vexceptions);
}
commit 95ed84c9326f55e85f9e8513cd8284e05ec42e17
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Feb 19 15:13:41 2015 +0100
Copy the master's sequence to a new exception
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 7b4c639..5e6f3b3 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -977,6 +977,9 @@ class kolab_driver extends calendar_driver
if ($reschedule) {
$event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
}
+ else if (!isset($event['sequence'])) {
+ $event['sequence'] = $master['sequence'];
+ }
// save properties to a recurrence exception instance
if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) {
commit ac2bd4700f55cdd48337f55240bc37066d309320
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Feb 19 15:13:13 2015 +0100
Store recurrence-id for single (non-recurring) events and use for iTip replies
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 8038add..b4a7aee 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -364,7 +364,7 @@ class libcalendaring_itip
// check if the given itip object matches the last state
if ($existing) {
- $latest = (isset($event['sequence']) && $existing['sequence'] == $event['sequence']) ||
+ $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) ||
(!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']);
}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index c203854..2d9c3cc 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1411,7 +1411,7 @@ class libcalendaring extends rcube_plugin
public static function identify_recurrence_instance(&$object)
{
// for savemode=all, remove recurrence instance identifiers
- if (!empty($object['_savemode']) && $object['_savemode'] == 'all') {
+ if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) {
unset($object['_instance'], $object['recurrence_date']);
}
// set instance and 'savemode' according to recurrence-id
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 979b33b..0c90e85 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -122,6 +122,9 @@ class kolab_format_event extends kolab_format_xcal
}
$this->obj->setExceptions($vexceptions);
}
+ else if ($object['recurrence_date'] && $object['recurrence_date'] instanceof DateTime) {
+ $this->obj->setRecurrenceID(self::get_datetime($object['recurrence_date'], null, $object['allday']), (bool)$object['thisandfuture']);
+ }
// cache this data
$this->data = $object;
commit b47b13a35ebb65cecbe028a177deed10752ee7cc
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed Feb 18 11:30:16 2015 +0100
Dynamically update attendees on exceptions in 'all' and 'future' save mode
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index f2a9269..7b4c639 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1045,19 +1045,41 @@ class kolab_driver extends calendar_driver
$event['end'] = $master['end'];
}
+ // when saving an instance in 'all' mode, copy recurrence exceptions over
+ if ($old['recurrence_id']) {
+ $event['recurrence'] = $master['recurrence'];
+ }
+
// 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);
- // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
- if (($old_start_date != $new_start_date || $old_start_time != $new_start_time) &&
- is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
- $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
- $recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
- rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
- if (is_a($recurrence_id, 'DateTime')) {
- $recurrence_id->add($date_shift);
- $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
- $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
+ self::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
+ }
+
+ // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
+ if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) {
+ $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
+ foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ $recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
+ rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
+ if (is_a($recurrence_id, 'DateTime')) {
+ $recurrence_id->add($date_shift);
+ $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
+ $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
+ }
}
}
}
@@ -1121,6 +1143,22 @@ class kolab_driver extends calendar_driver
$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']) {
@@ -1139,7 +1177,11 @@ class kolab_driver extends calendar_driver
// 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);
+ 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);
+ }
}
}
/*
@@ -1174,10 +1216,14 @@ class kolab_driver extends calendar_driver
*
* @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)
+ public static function merge_exception_data(&$event, $overlay, $blacklist = null)
{
- static $forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
+ $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) {
@@ -1213,7 +1259,7 @@ class kolab_driver extends calendar_driver
* @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)
+ public static function merge_attendee_data(&$event, $attendees, $removed = null)
{
if (!empty($attendees) && !is_array($attendees[0])) {
$attendees = array($attendees);
@@ -1234,6 +1280,13 @@ class kolab_driver extends calendar_driver
$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);
+ });
+ }
}
/**
commit 422bb0a298c1867fba7626b953b7f263492a3ab1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed Feb 18 10:21:15 2015 +0100
Fix RSVP flag in iCal and storage (defaults to false); remove redundant information from ical PRODID
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 10c2223..07612d5 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -41,7 +41,7 @@ class libvcalendar implements Iterator
{
private $timezone;
private $attach_uri = null;
- private $prodid = '-//Roundcube//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
+ private $prodid = '-//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
private $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE',
'cutype' => 'CUTYPE', 'rsvp' => 'RSVP', 'delegated-from' => 'DELEGATED-FROM', 'delegated-to' => 'DELEGATED-TO');
@@ -64,7 +64,7 @@ class libvcalendar implements Iterator
function __construct($tz = null)
{
$this->timezone = $tz;
- $this->prodid = '-//Roundcube//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+ $this->prodid = '-//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
}
/**
@@ -502,7 +502,7 @@ class libvcalendar implements Iterator
case 'ATTENDEE':
case 'ORGANIZER':
- $params = array();
+ $params = array('rsvp' => false);
foreach ($prop->parameters as $param) {
switch ($param->name) {
case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 3d7bc27..e6507d6 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -357,7 +357,7 @@ abstract class kolab_format_xcal extends kolab_format
// set attendee RSVP if missing
if (!isset($attendee['rsvp'])) {
- $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = true;
+ $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = $reschedule;
}
$att = new Attendee;
commit c7df74d5d0cdf3b292a1034c9a0bb3007e9e0b23
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed Feb 18 10:20:00 2015 +0100
Fix updating attendees (do not accidentally set exceptions to thisandfuture)
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index ff02fbb..f2a9269 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -642,7 +642,7 @@ class kolab_driver extends calendar_driver
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'])) {
+ if (($event['_savemode'] == 'future' && $event['recurrence_id']) || (!empty($event['recurrence']) && !$event['recurrence_id'])) {
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
commit f7e7df62a28541b0d3bcecf77ffc2819e006924f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Feb 17 15:49:14 2015 +0100
Apply date offset from exceptions to recurring occurrences (#4386)
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 4316542..a392a9f 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -297,7 +297,7 @@ class kolab_calendar extends kolab_storage_folder_api
$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_event_data($event, $exception);
+ kolab_driver::merge_exception_data($event, $exception);
}
}
}
@@ -641,7 +641,7 @@ class kolab_calendar extends kolab_storage_folder_api
$rec_event['_instance'] = $instance_id;
if ($overlay_data || $exdata[$datestr]) // copy data from exception
- kolab_driver::merge_event_data($rec_event, $exdata[$datestr] ?: $overlay_data);
+ kolab_driver::merge_exception_data($rec_event, $exdata[$datestr] ?: $overlay_data);
$rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['uid'];
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 0a9790c..ff02fbb 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -928,6 +928,10 @@ 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');
+
+ // 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
@@ -1135,7 +1139,7 @@ class kolab_driver extends calendar_driver
// merge the new event properties onto future exceptions
if ($savemode == 'future' && $exception['_instance'] >= $old['_instance']) {
unset($event['thisandfuture']);
- self::merge_event_data($master['recurrence']['EXCEPTIONS'][$i], $event);
+ self::merge_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event);
}
}
/*
@@ -1171,19 +1175,28 @@ class kolab_driver extends calendar_driver
* @param array The event object to be altered
* @param array The overlay event object to be merged over $event
*/
- public static function merge_event_data(&$event, $overlay)
+ public static function merge_exception_data(&$event, $overlay)
{
static $forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
+ // 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) {
- // 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')));
+ 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']) {
commit 4d534ea7868538c517fc7af4ae80d6db411a961a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Feb 17 15:47:12 2015 +0100
Forward savemode when removing a cancelled event
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 2eec27c..8038add 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -602,7 +602,11 @@ class libcalendaring_itip
// for CANCEL messages, we can:
else if ($method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
- $event_prop = array_filter(array('uid' => $event['uid'], '_instance' => $event['_instance']));
+ $event_prop = array_filter(array(
+ 'uid' => $event['uid'],
+ '_instance' => $event['_instance'],
+ '_savemode' => $event['_savemode'],
+ ));
// 1. remove the event from our calendar
$button_remove = html::tag('input', array(
commit 46866e76cc00433e34a8c6c7c2967d5dbf6be6d0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Feb 17 15:03:39 2015 +0100
Report cancellation to removed attendees with this-and-future parameter
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 571e68b..fc4bbff 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1149,6 +1149,10 @@ class calendar extends rcube_plugin
$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'];
commit 3ea6d4357926092de2080667cae7c6c516e33097
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Feb 17 14:54:12 2015 +0100
Fix deletion/cancellation of this-and-future instances
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 8e6a1a3..571e68b 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1711,6 +1711,9 @@ class calendar extends rcube_plugin
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) {
$event['attendees'][$i]['noreply'] = true;
}
+ else {
+ unset($event['attendees'][$i]['noreply']);
+ }
}
if ($organizer === null && !empty($event['organizer'])) {
@@ -2432,12 +2435,14 @@ class calendar extends rcube_plugin
*/
function event_itip_remove()
{
- $success = false;
- $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
- $inst = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $success = false;
+ $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
+ $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
// search for event if only UID is given
- if ($event = $this->driver->get_event(array('uid' => $uid, '_instance' => $inst), true)) {
+ if ($event = $this->driver->get_event(array('uid' => $uid, '_instance' => $instance), true)) {
+ $event['_savemode'] = $savemode;
$success = $this->driver->remove_event($event, true);
}
diff --git a/plugins/calendar/skins/larry/print.css b/plugins/calendar/skins/larry/print.css
index ce6e8e7..fc5de97 100644
--- a/plugins/calendar/skins/larry/print.css
+++ b/plugins/calendar/skins/larry/print.css
@@ -76,6 +76,7 @@ body, td, th, div, p, h3, select, input, textarea {
#calendarlist li div {
float: left;
padding-right: 3em;
+ padding-bottom: 1em;
}
#calendarlist input,
commit ba84648fa7031edd41d949e82e62e5fb80e482df
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Feb 17 14:04:57 2015 +0100
- Onyl print events from active calendars (#4603)
- Fix colors of events in month view
- Show calendar color legend as floating list, no hierarchy
diff --git a/plugins/calendar/print.js b/plugins/calendar/print.js
index c3c647c..941d04c 100644
--- a/plugins/calendar/print.js
+++ b/plugins/calendar/print.js
@@ -43,6 +43,9 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
var src, event_sources = [];
var add_url = (rcmail.env.search ? '&q='+escape(rcmail.env.search) : '');
for (var id in rcmail.env.calendars) {
+ if (!rcmail.env.calendars[id].active)
+ continue;
+
source = $.extend({
url: "./?_task=calendar&_action=load_events&source=" + escape(id) + add_url,
className: 'fc-event-cal-'+id,
diff --git a/plugins/calendar/skins/larry/print.css b/plugins/calendar/skins/larry/print.css
index 5d190f5..ce6e8e7 100644
--- a/plugins/calendar/skins/larry/print.css
+++ b/plugins/calendar/skins/larry/print.css
@@ -1,7 +1,7 @@
/*** Printing styles for Calendar plugin ***/
body {
- margin: 0;
+ margin: 0 0 1em 0;
color: #000;
background: #fff;
}
@@ -54,18 +54,30 @@ body, td, th, div, p, h3, select, input, textarea {
}
#calendarlist {
- list-style-type: square;
+ list-style: none;
margin: 2em 0;
padding-left: 1em;
}
+#calendarlist ul {
+ float: left;
+ list-style: none;
+ padding-left: 0;
+}
+
#calendarlist li {
+ float: left;
padding-left: 0;
- padding-right: 3em;
+ padding-right: 0;
margin-left: 0;
font-weight: bold;
}
+#calendarlist li div {
+ float: left;
+ padding-right: 3em;
+}
+
#calendarlist input,
#calendarlist .handle {
display: none;
@@ -207,6 +219,9 @@ body, td, th, div, p, h3, select, input, textarea {
font-style: italic;
}
+.fc-view-month .fc-event-hori .fc-event-inner {
+ background: #fff !important;
+}
.fc-view-table col.fc-event-location {
width: 20%;
diff --git a/plugins/calendar/skins/larry/templates/print.html b/plugins/calendar/skins/larry/templates/print.html
index 8d7789a..e679f72 100644
--- a/plugins/calendar/skins/larry/templates/print.html
+++ b/plugins/calendar/skins/larry/templates/print.html
@@ -19,6 +19,7 @@
<div class="calwidth">
<roundcube:object name="plugin.calendar_list" activeonly="true" id="calendarlist" />
+ <br style="clear:both">
</div>
<roundcube:object name="plugin.calendar_css" printmode="true" />
commit f78af8b09f6f20858d4a8f6a69e590c03befdaa1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Feb 17 13:10:37 2015 +0100
Fix ical export after last commit
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 29bc01e..4316542 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -291,7 +291,7 @@ class kolab_calendar extends kolab_storage_folder_api
}
// find and merge exception for the first instance
- if (!empty($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS'])) {
+ 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);
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 3e3f0fc..0a9790c 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1674,10 +1674,10 @@ class kolab_driver extends calendar_driver
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 = $event['allday'] ? 'Ymd' : 'Ymd\THis';
- $record['recurrence_date'] = $record['start'];
- $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
+ $recurrence_id_format = $record['allday'] ? 'Ymd' : 'Ymd\THis';
+ $record['_instance'] = $record['start']->format($recurrence_id_format);
}
// remove internals
commit 8a90069071821f4cf2cdac6d7fce82cf12a32c11
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Feb 17 11:36:01 2015 +0100
- Support exceptions and iTip messages with thisansfuture range
- Store two exceptions for the same occurence if necessary (with differing range)
- Update attendee status from iTip REPLY to all exceptions stored for the event
- Correctly handle exceptions on the first instance (main event)
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 786cd76..8e6a1a3 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -863,6 +863,7 @@ class calendar extends rcube_plugin
$this->write_preprocess($event, $action);
if ($success = $this->driver->new_event($event)) {
$event['id'] = $event['uid'];
+ $event['_savemode'] = 'all';
$this->cleanup_event($event);
}
$reload = $success && $event['recurrence'] ? 2 : 1;
@@ -1017,11 +1018,18 @@ class calendar extends rcube_plugin
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
$event['comment'] = $reply_comment;
+ $event['thisandfuture'] = $event['_savemode'] == 'future';
if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
+
+ // refresh all calendars
+ if ($event['calendar'] != $ev['calendar']) {
+ $this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true));
+ $reload = 0;
+ }
}
break;
@@ -1139,11 +1147,13 @@ class calendar extends rcube_plugin
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
+ $event['_savemode'] = $_savemode;
// 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)
@@ -2763,9 +2773,11 @@ class calendar extends rcube_plugin
}
}
$event_attendee = null;
+ $update_attendees = array();
foreach ($event['attendees'] as $attendee) {
if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
$event_attendee = $attendee;
+ $update_attendees[] = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
@@ -2775,9 +2787,12 @@ class calendar extends rcube_plugin
}
// also copy delegate attendee
else if (!empty($attendee['delegated-from']) &&
- (stripos($attendee['delegated-from'], $event['_sender']) !== false || stripos($attendee['delegated-from'], $event['_sender_utf']) !== false) &&
- (!in_array($attendee['email'], $existing_attendee_emails))) {
- $existing['attendees'][] = $attendee;
+ (stripos($attendee['delegated-from'], $event['_sender']) !== false ||
+ stripos($attendee['delegated-from'], $event['_sender_utf']) !== false)) {
+ $update_attendees[] = $attendee;
+ if (!in_array($attendee['email'], $existing_attendee_emails)) {
+ $existing['attendees'][] = $attendee;
+ }
}
}
@@ -2794,12 +2809,12 @@ class calendar extends rcube_plugin
// found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $event_attendee) {
$existing['attendees'][$existing_attendee] = $event_attendee;
- $success = $this->driver->edit_event($existing);
+ $success = $this->driver->update_attendees($existing, $update_attendees);
}
// update the entire attendees block
else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
$existing['attendees'][] = $event_attendee;
- $success = $this->driver->edit_event($existing);
+ $success = $this->driver->update_attendees($existing, $update_attendees);
}
else {
$error_msg = $this->gettext('newerversionexists');
@@ -2855,7 +2870,7 @@ class calendar extends rcube_plugin
// if the RSVP reply only refers to a single instance:
// store unmodified master event with current instance as exception
- if (!empty($instance) && $savemode != 'all') {
+ 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
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 742830b..aecd2e1 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -205,6 +205,18 @@ abstract class calendar_driver
}
/**
+ * 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);
+ }
+
+ /**
* Move a single event
*
* @param array Hash array with event properties:
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index b5b272f..29bc01e 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -195,7 +195,11 @@ class kolab_calendar extends kolab_storage_folder_api
if ($master_id != $id && ($record = $this->storage->get_object($master_id)))
$this->events[$master_id] = $this->_to_rcube_event($record);
- if (($master = $this->events[$master_id]) && $master['recurrence']) {
+ // check for match on the first instance already
+ if (($_instance = $this->events[$master_id]['_instance']) && $id == $master_id . '-' . $_instance) {
+ $this->events[$id] = $this->events[$master_id];
+ }
+ else if (($master = $this->events[$master_id]) && $master['recurrence']) {
$this->get_recurring_events($record, $master['start'], null, $id);
}
}
@@ -274,17 +278,10 @@ class kolab_calendar extends kolab_storage_folder_api
$add = true;
// skip the first instance of a recurring event if listed in exdate
- if ($virtual && (!empty($event['recurrence']['EXDATE']) || !empty($event['recurrence']['EXCEPTIONS']))) {
+ if ($virtual && !empty($event['recurrence']['EXDATE'])) {
$event_date = $event['start']->format('Ymd');
$exdates = (array)$event['recurrence']['EXDATE'];
- // add dates from exceptions to list
- if (is_array($event['recurrence']['EXCEPTIONS'])) {
- foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
- $exdates[] = clone $exception['start'];
- }
- }
-
foreach ($exdates as $exdate) {
if ($exdate->format('Ymd') == $event_date) {
$add = false;
@@ -293,6 +290,18 @@ class kolab_calendar extends kolab_storage_folder_api
}
}
+ // find and merge exception for the first instance
+ if (!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_event_data($event, $exception);
+ }
+ }
+ }
+
if ($add)
$events[] = $event;
}
@@ -583,24 +592,29 @@ class kolab_calendar extends kolab_storage_folder_api
$rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
$rec_event['isexception'] = 1;
- // found the specifically requested instance, exiting...
- if ($rec_event['id'] == $event_id) {
+ // found the specifically requested instance: register exception (single occurrence wins)
+ if ($rec_event['id'] == $event_id && (!$this->events[$event_id] || $this->events[$event_id]['thisandfuture'])) {
$rec_event['recurrence'] = $recurrence_rule;
$rec_event['recurrence_id'] = $event['uid'];
- $events[] = $rec_event;
$this->events[$rec_event['id']] = $rec_event;
- return $events;
}
// remember this exception's date
$exdate = substr($exception['_instance'], 0, 8);
- $exdata[$exdate] = $rec_event;
+ if (!$exdata[$exdate] || $exdata[$exdate]['thisandfuture']) {
+ $exdata[$exdate] = $rec_event;
+ }
if ($rec_event['thisandfuture']) {
$futuredata[$exdate] = $rec_event;
}
}
}
+ // found the specifically requested instance, exiting...
+ if ($event_id && !empty($this->events[$event_id])) {
+ return array($this->events[$event_id]);
+ }
+
// use libkolab to compute recurring events
if (class_exists('kolabcalendaring')) {
$recurrence = new kolab_date_recurrence($object);
@@ -627,7 +641,7 @@ class kolab_calendar extends kolab_storage_folder_api
$rec_event['_instance'] = $instance_id;
if ($overlay_data || $exdata[$datestr]) // copy data from exception
- $this->_merge_event_data($rec_event, $exdata[$datestr] ?: $overlay_data);
+ kolab_driver::merge_event_data($rec_event, $exdata[$datestr] ?: $overlay_data);
$rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['uid'];
@@ -652,37 +666,10 @@ class kolab_calendar extends kolab_storage_folder_api
}
/**
- * 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','recurrence','recurrence_date','organizer','_attachments');
-
- foreach ($overlay as $prop => $value) {
- // adjust time of the recurring event instance
- if ($prop == 'start' || $prop == 'end') {
- if (is_object($event[$prop]) && is_a($event[$prop], 'DateTime')) {
- $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
- // set date value if overlay is an exception of the current instance
- if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
- $event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
- }
- }
- }
- else if ($prop[0] != '_' && !in_array($prop, $forbidden))
- $event[$prop] = $value;
- }
- }
-
- /**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_event($record)
{
- $record['id'] = $record['uid'];
$record['calendar'] = $this->id;
$record['links'] = $this->get_links($record['uid']);
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 0cd962d..3e3f0fc 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -537,7 +537,7 @@ class kolab_driver extends calendar_driver
$id = $event['id'] ?: $event['uid'];
$cal = $event['calendar'];
- // we're looking for a recurring instance: expand the ID to our internal convention for recurring instanced
+ // 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'];
}
@@ -634,6 +634,48 @@ class kolab_driver extends calendar_driver
return $ret;
}
+ /**
+ * Update the participant status for the given attendees
+ *
+ * @see calendar_driver::update_attendees()
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ // for this-and-future updates, merge the updated attendees onto all exceptions in range
+ if (($event['_savemode'] == 'future' && $event['recurrence_id']) || !empty($event['recurrence'])) {
+ 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
@@ -933,15 +975,10 @@ class kolab_driver extends calendar_driver
}
// save properties to a recurrence exception instance
- if ($old['recurrence_id'] && is_array($master['recurrence']['EXCEPTIONS'])) {
- foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
- if ($exception['_instance'] == $old['_instance']) {
- $event['_instance'] = $old['_instance'];
- $event['recurrence_date'] = $old['recurrence_date'];
- $master['recurrence']['EXCEPTIONS'][$i] = $event;
- $success = $storage->update_event($master, $old['id']);
- break 2;
- }
+ if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) {
+ if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) {
+ $success = $storage->update_event($master, $old['id']);
+ break;
}
}
@@ -962,7 +999,7 @@ class kolab_driver extends calendar_driver
// save as new exception to master event
if ($add_exception) {
$event['_instance'] = $old['_instance'];
- $event['recurrence_date'] = $old['recurrence_date'];
+ $event['recurrence_date'] = $old['recurrence_date'] ?: $old['start'];
$master['recurrence']['EXCEPTIONS'][] = $event;
}
$success = $storage->update_event($master);
@@ -1004,6 +1041,8 @@ class kolab_driver extends calendar_driver
$event['end'] = $master['end'];
}
+ // TODO: forward changes to exceptions (which do not yet have differing values stored)
+
// adjust recurrence-id when start changed and therefore the entire recurrence chain changes
if (($old_start_date != $new_start_date || $old_start_time != $new_start_time) &&
is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
@@ -1071,6 +1110,120 @@ class kolab_driver extends calendar_driver
}
/**
+ * Apply the given changes to already existing exceptions
+ */
+ protected function update_recurrence_exceptions(&$master, $event, $old, $savemode)
+ {
+ $saved = false;
+ $existing = null;
+
+ 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_event_data($master['recurrence']['EXCEPTIONS'][$i], $event);
+ }
+ }
+/*
+ // 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;
+ }
+
+ /**
+ * 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
+ */
+ public static function merge_event_data(&$event, $overlay)
+ {
+ static $forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
+
+ 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 == '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)
+ {
+ 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;
+ }
+ }
+ }
+
+ /**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
@@ -1520,6 +1673,13 @@ class kolab_driver extends calendar_driver
if (empty($record['recurrence']))
unset($record['recurrence']);
+ // add instance identifier to first occurrence (master event)
+ if ($record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) {
+ $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
+ $record['recurrence_date'] = $record['start'];
+ $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
+ }
+
// remove internals
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 63a0548..c203854 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1410,8 +1410,12 @@ class libcalendaring extends rcube_plugin
*/
public static function identify_recurrence_instance(&$object)
{
+ // for savemode=all, remove recurrence instance identifiers
+ if (!empty($object['_savemode']) && $object['_savemode'] == 'all') {
+ unset($object['_instance'], $object['recurrence_date']);
+ }
// set instance and 'savemode' according to recurrence-id
- if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
+ 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';
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index ca7d1fd..e5e0426 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -109,7 +109,7 @@ $labels['acceptattendee'] = 'Accept participant';
$labels['declineattendee'] = 'Decline participant';
$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
$labels['rsvpmodeall'] = 'The entire series';
-$labels['rsvpmodecurrent'] = 'This occurrence';
+$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';
commit 7fd2eb873dbeed5fb955e459b20e1134c09892b0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Feb 16 18:45:25 2015 +0100
Hide RSVP-mode menu when moving/resizing event dialog
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index fcd360a..c9334ec 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -604,13 +604,16 @@ function rcube_calendar_ui(settings)
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
- rcmail.command('menu-close','eventoptionsmenu')
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
},
dragStart: function() {
- rcmail.command('menu-close','eventoptionsmenu')
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
},
resizeStart: function() {
- rcmail.command('menu-close','eventoptionsmenu')
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
},
buttons: buttons,
minWidth: 320,
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index cd59f66..71c101d 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -967,6 +967,7 @@ rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback)
$.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);
});
commit 6a5a8148348d0eddb452ff33affe4af87e9359cc
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Feb 16 15:59:50 2015 +0100
Don't remove properties from exceptions which are equal to the master event. KE17 says:
A recurrence exception SHALL copy ALL properties of the base event, and adjust as required,
and it SHALL NOT be applied on top of the orginial event properties (The exception replaces
the complete original event definition for the specific occurrence).
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 0fda1e3..979b33b 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -241,7 +241,6 @@ class kolab_format_event extends kolab_format_xcal
private function compact_exception($exception, $master)
{
$forbidden = array('recurrence','organizer','_attachments');
- $whitelist = array('start','end');
foreach ($forbidden as $prop) {
if (array_key_exists($prop, $exception)) {
@@ -249,12 +248,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 && !in_array($prop, $whitelist)) {
- unset($exception[$prop]);
- }
- }
-
// preserve this property for date serialization
$exception['allday'] = $master['allday'];
commit fe64e05e48c11b809bde1a32161dcdce3e1e2ea3
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Feb 16 15:36:25 2015 +0100
Render a menu to select the RSVP mode for recurring events instead of using radio buttons
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index ec83d1b..fcd360a 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -557,11 +557,11 @@ function rcube_calendar_ui(settings)
if (event.recurrence && event.id) {
var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
- $('#event-rsvp input.rsvp-replymode[value="'+sel+'"]').prop('checked', true);
- $('#event-rsvp .rsvp-replymode-message').show();
+ $('#event-rsvp .rsvp-buttons').addClass('recurring');
+ }
+ else {
+ $('#event-rsvp .rsvp-buttons').removeClass('recurring');
}
- else
- $('#event-rsvp .rsvp-replymode-message').hide();
}
var buttons = [];
@@ -2378,14 +2378,31 @@ function rcube_calendar_ui(settings)
}
// when the user accepts or declines an event invitation
- var event_rsvp = function(response, delegate)
+ var event_rsvp = function(response, delegate, replymode)
{
+ var btn;
+ if (typeof response == 'object') {
+ btn = $(response);
+ response = btn.attr('rel')
+ }
+ else {
+ btn = $('#event-rsvp input.button[rel='+response+']');
+ }
+
+ // show menu to select rsvp reply mode (current or all)
+ if (me.selected_event && me.selected_event.recurrence && !replymode) {
+ rcube_libcalendaring.itip_rsvp_recurring(btn, function(resp, mode) {
+ event_rsvp(resp, null, mode);
+ });
+ return;
+ }
+
if (me.selected_event && me.selected_event.attendees && response) {
// bring up delegation dialog
if (response == 'delegated' && !delegate) {
rcube_libcalendaring.itip_delegate_dialog(function(data) {
data.rsvp = data.rsvp ? 1 : '';
- event_rsvp('delegated', data);
+ event_rsvp('delegated', data, replymode);
});
return;
}
@@ -2419,7 +2436,7 @@ function rcube_calendar_ui(settings)
}
// submit status change to server
- var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val(), _savemode: $('input.rsvp-replymode:checked').val() }, (delegate || {})),
+ var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val(), _savemode: replymode || 'all' }, (delegate || {})),
noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
// import event from mail (temporary iTip event)
@@ -4163,7 +4180,7 @@ function rcube_calendar_ui(settings)
});
$('#event-rsvp input.button').click(function(e) {
- event_rsvp($(this).attr('rel'))
+ event_rsvp(this)
});
$('#eventedit input.edit-recurring-savemode').change(function(e) {
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 0fec69c..fef16bd 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1063,24 +1063,8 @@ td.topalign {
text-align: center;
}
-.event-dialog-message .rsvp-replymode-message {
- margin-top: 0.8em;
- margin-bottom: 0.6em;
-}
-
-.event-dialog-message .rsvp-replymode-message .replymode-select {
- padding-left: 22px;
-}
-
-.event-dialog-message .rsvp-replymode-message label {
- color: inherit;
- margin-right: 0.4em;
- white-space: nowrap;
- min-width: 4em;
-}
-
-.event-dialog-message .rsvp-replymode-message input.rsvp-replymode {
- margin-right: 0.4em;
+.libcal-rsvp-replymode li a {
+ cursor: default;
}
#event-rsvp,
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index f56463c..2eec27c 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -677,20 +677,16 @@ class libcalendaring_itip
}
}
+ foreach (array('all','current','future') as $mode) {
+ $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode"));
+ }
+
$savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode'));
return html::div($attrib,
html::div('label', $this->gettext('acceptinvitation')) .
html::div('rsvp-buttons',
$buttons .
- html::div(array('class' => 'rsvp-replymode-message', 'style' => 'display:none'),
- html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->gettext('rsvprecurringevent')) .
- html::div('replymode-select',
- html::label(null, $savemode_radio->show('all', array('value' => 'all')) . $this->gettext('allevents')) .
- html::label(null, $savemode_radio->show(null, array('value' => 'current')) . $this->gettext('currentevent')) .
- html::label(null, $savemode_radio->show(null, array('value' => 'future')) . $this->gettext('futurevents'))
- )
- ) .
html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
)
);
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index a13ebf7..cd59f66 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -959,6 +959,39 @@ rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
};
/**
+ * 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>')
+ .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(event, task, title)
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 992113a..ca7d1fd 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -108,7 +108,9 @@ $labels['acceptinvitation'] = 'Do you accept this invitation?';
$labels['acceptattendee'] = 'Accept participant';
$labels['declineattendee'] = 'Decline participant';
$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
-$labels['rsvprecurringevent'] = 'This is a series of events! Does your response apply to all, this occurrence only or this and future occurrences?';
+$labels['rsvpmodeall'] = 'The entire series';
+$labels['rsvpmodecurrent'] = 'This occurrence';
+$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';
commit d7733e7879c7c16074d11ebf58515c50dddf53bb
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Feb 16 11:06:42 2015 +0100
Allow to RSVP reply on a single occurence when viewing the event in the calendar preview
This will copy the main event from the iTip invitation with unchanged partstat into the
user calendar and register a recurrence exception with the selected partsat and send a
reply for this occurrence only.
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 7682ba0..786cd76 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2655,6 +2655,8 @@ class calendar extends rcube_plugin
$delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
$noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST));
$noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
+ $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
$error_msg = $this->gettext('errorimportingevent');
$success = false;
@@ -2747,7 +2749,7 @@ class calendar extends rcube_plugin
if ($existing) {
// forward savemode for correct updates of recurring events
- $existing['_savemode'] = $event['_savemode'];
+ $existing['_savemode'] = $savemode ?: $event['_savemode'];
// only update attendee status
if ($event['_method'] == 'REPLY') {
@@ -2850,9 +2852,43 @@ class calendar extends rcube_plugin
if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
$event['free_busy'] = 'free';
}
+
+ // if the RSVP reply only refers to a single instance:
+ // store unmodified master event with current instance as exception
+ if (!empty($instance) && $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
- $event['calendar'] = $calendar['id'];
- $success = $this->driver->new_event($event);
+ if (!$master) {
+ $event['calendar'] = $calendar['id'];
+ $success = $this->driver->new_event($event);
+ }
}
else if ($status == 'declined')
$error_msg = null;
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index f9e0d28..ec83d1b 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2434,6 +2434,7 @@ function rcube_calendar_ui(settings)
_rsvp: (delegate && delegate.rsvp) ? 1 : 0,
_noreply: noreply,
_comment: submit_data.comment,
+ _instance: submit_data._instance,
_savemode: submit_data._savemode
});
}
commit aaaa9c58185d96a120893f2f3e3c361d44309e8a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Feb 16 11:00:26 2015 +0100
Take differing parstat values in recurrence exceptions into account when querying for pending/declined/regular events:
- Colelct partstat tags from recurrence exceptions when caching
- Querying for 'tags != x-partstat:<email>:needs-action' may miss some valid records
- Do post-filtering on all events, including recurring instances
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index e402db9..742830b 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -450,6 +450,7 @@ abstract class calendar_driver
$rcmail = rcmail::get_instance();
$recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
+ $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
// determine a reasonable end date if none given
if (!$end) {
@@ -465,10 +466,10 @@ abstract class calendar_driver
$i = 0;
while ($next_event = $recurrence->next_instance()) {
- $next_event['uid'] = $event['uid'] . '-' . ++$i;
// add to output if in range
if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
- $next_event['id'] = $next_event['uid'];
+ $next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
+ $next_event['id'] = $next_event['uid'] . '-' . $exception['_instance'];
$next_event['recurrence_id'] = $event['uid'];
$events[] = $next_event;
}
@@ -477,7 +478,7 @@ abstract class calendar_driver
}
// avoid endless recursion loops
- if ($i > 1000) {
+ if (++$i > 1000) {
break;
}
}
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 06a8244..b5b272f 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -236,35 +236,31 @@ class kolab_calendar extends kolab_storage_folder_api
$query[] = array('dtstart', '<=', $end);
$query[] = array('dtend', '>=', $start);
- // add query to exclude pending/declined invitations
- if (empty($filter_query) && $this->get_namespace() != 'other') {
- foreach ($user_emails as $email) {
- $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
- $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
- }
- }
- else if (is_array($filter_query)) {
+ if (is_array($filter_query)) {
$query = array_merge($query, $filter_query);
}
if (!empty($search)) {
$search = mb_strtolower($search);
+ $words = rcube_utils::tokenize_string($search, 1);
foreach (rcube_utils::normalize_string($search, true) as $word) {
$query[] = array('words', 'LIKE', $word);
}
}
+ else {
+ $words = array();
+ }
+
+ // set partstat filter to skip pending and declined invitations
+ if (empty($filter_query) && $this->get_namespace() != 'other') {
+ $partstat_exclude = array('NEEDS-ACTION','DECLINED');
+ }
+ else {
+ $partstat_exclude = array();
+ }
$events = array();
foreach ($this->storage->select($query) as $record) {
- // post-filter events to skip pending and declined invitations
- if (empty($filter_query) && is_array($record['attendees']) && $this->get_namespace() != 'other') {
- foreach ($record['attendees'] as $attendee) {
- if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], array('NEEDS-ACTION','DECLINED'))) {
- continue 2;
- }
- }
- }
-
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
@@ -272,18 +268,6 @@ class kolab_calendar extends kolab_storage_folder_api
if ($event['categories'])
$this->categories[$event['categories']]++;
- // filter events by search query
- if (!empty($search)) {
- $hits = 0;
- $words = rcube_utils::tokenize_string($search, 1);
- foreach ($words as $word) {
- $hits += $this->_fulltext_match($event, $word);
- }
-
- if ($hits < count($words)) // skip this event if not match with search term
- continue;
- }
-
// list events in requested time window
if ($event['start'] <= $end && $event['end'] >= $start) {
unset($event['_attendees']);
@@ -312,24 +296,38 @@ class kolab_calendar extends kolab_storage_folder_api
if ($add)
$events[] = $event;
}
-
+
// resolve recurring events
if ($record['recurrence'] && $virtual == 1) {
$events = array_merge($events, $this->get_recurring_events($record, $start, $end));
+ }
+ }
- // when searching, only recurrence exceptions may match the query so post-filter the results again
- if (!empty($search) && $record['recurrence']['EXCEPTIONS']) {
- $me = $this;
- $events = array_filter($events, function($event) use ($words, $me) {
- $hits = 0;
- foreach ($words as $word) {
- $hits += $me->_fulltext_match($event, $word, false);
- }
- return $hits >= count($words);
- });
+ // post-filter all events by fulltext search and partstat values
+ $me = $this;
+ $events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
+ // fulltext search
+ if (count($words)) {
+ $hits = 0;
+ foreach ($words as $word) {
+ $hits += $me->_fulltext_match($event, $word, false);
+ }
+ if ($hits < count($words)) {
+ return false;
}
}
- }
+
+ // partstat filter
+ if (count($partstat_exclude) && is_array($event['attendees'])) {
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $partstat_exclude)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ });
// avoid session race conditions that will loose temporary subscriptions
$this->cal->rc->session->nowrite = true;
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index a5b0f73..0fda1e3 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -223,15 +223,16 @@ class kolab_format_event extends kolab_format_xcal
*
* @return array List of tags to save in cache
*/
- public function get_tags()
+ public function get_tags($obj = null)
{
- $tags = parent::get_tags();
+ $tags = parent::get_tags($obj);
+ $object = $obj ?: $this->data;
- foreach ((array)$this->data['categories'] as $cat) {
+ foreach ((array)$object['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
- return $tags;
+ return array_unique($tags);
}
/**
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index d3ddfe9..4640875 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -121,20 +121,21 @@ class kolab_format_task extends kolab_format_xcal
*
* @return array List of tags to save in cache
*/
- public function get_tags()
+ public function get_tags($obj = null)
{
- $tags = parent::get_tags();
+ $tags = parent::get_tags($obj);
+ $object = $obj ?: $this->data;
- if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
+ if ($object['status'] == 'COMPLETED' || ($object['complete'] == 100 && empty($object['status'])))
$tags[] = 'x-complete';
- if ($this->data['priority'] == 1)
+ if ($object['priority'] == 1)
$tags[] = 'x-flagged';
- if ($this->data['parent_id'])
- $tags[] = 'x-parent:' . $this->data['parent_id'];
+ if ($object['parent_id'])
+ $tags[] = 'x-parent:' . $object['parent_id'];
- return $tags;
+ return array_unique($tags);
}
}
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 6d49ad1..3d7bc27 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -611,23 +611,31 @@ abstract class kolab_format_xcal extends kolab_format
*
* @return array List of tags to save in cache
*/
- public function get_tags()
+ public function get_tags($obj = null)
{
$tags = array();
+ $object = $obj ?: $this->data;
- if (!empty($this->data['valarms'])) {
+ if (!empty($object['valarms'])) {
$tags[] = 'x-has-alarms';
}
// create tags reflecting participant status
- if (is_array($this->data['attendees'])) {
- foreach ($this->data['attendees'] as $attendee) {
+ if (is_array($object['attendees'])) {
+ foreach ($object['attendees'] as $attendee) {
if (!empty($attendee['email']) && !empty($attendee['status']))
$tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
}
}
- return $tags;
+ // collect tags from recurrence exceptions
+ if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
+ foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
+ $tags = array_merge($tags, $this->get_tags($exception));
+ }
+ }
+
+ return array_unique($tags);
}
/**
commit 8a74dc2d28aba4af89d123228fb638e4b4999dbd
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Sun Feb 15 19:09:10 2015 +0100
Don't copy recurrence_date to future occurrences
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index f61b681..7682ba0 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1676,6 +1676,7 @@ class calendar extends rcube_plugin
if ($event['recurrence']) {
$event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
$event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
+ unset($event['recurrence_date']);
}
foreach ((array)$event['attachments'] as $k => $attachment) {
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 15f030d..06a8244 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -661,7 +661,7 @@ class kolab_calendar extends kolab_storage_folder_api
*/
private function _merge_event_data(&$event, $overlay)
{
- static $forbidden = array('id','uid','recurrence','organizer','_attachments');
+ static $forbidden = array('id','uid','recurrence','recurrence_date','organizer','_attachments');
foreach ($overlay as $prop => $value) {
// adjust time of the recurring event instance
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index e414b87..0cd962d 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -962,6 +962,7 @@ class kolab_driver extends calendar_driver
// save as new exception to master event
if ($add_exception) {
$event['_instance'] = $old['_instance'];
+ $event['recurrence_date'] = $old['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][] = $event;
}
$success = $storage->update_event($master);
commit dbdce67e1e0c6675dc4bbaaa0d9056189328e1f0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Sun Feb 15 18:39:28 2015 +0100
Better distinction of 'current' and 'future' itip messages in UI and message text
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 53284a4..f56463c 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -125,7 +125,7 @@ class libcalendaring_itip
$recurrence_info = '';
if (!empty($event['recurrence_id'])) {
- $recurrence_info = "\n\n** " . $this->gettext('itip'.strtolower($method).'occurrenceonly') . ' **';
+ $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']));
@@ -742,7 +742,7 @@ class libcalendaring_itip
}
if (!empty($event['recurrence_date'])) {
$table->add('label', '');
- $table->add('recurrence-id', $this->gettext('itipsingleoccurrence'));
+ $table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence'));
}
else if (!empty($event['recurrence'])) {
$table->add('label', $this->gettext('recurring'));
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 31f08fd..992113a 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -111,9 +111,9 @@ $labels['declineattendeeconfirm'] = 'Enter a message to the declined participant
$labels['rsvprecurringevent'] = 'This is a series of events! Does your response apply to all, this occurrence only or this and future occurrences?';
$labels['itipsingleoccurrence'] = 'This is a <em>single occurrence</em> out of a series of events';
-$labels['itiprequestoccurrenceonly'] = 'The invitation only refers to this single occurrence';
-$labels['itipreplyoccurrenceonly'] = 'The response only refers to this single occurrence';
-$labels['itipcanceloccurrenceonly'] = 'The cancellation only refers to this single occurrence';
+$labels['itipfutureoccurrence'] = 'Refers to <em>this and all future occurrences</em> of a series of events';
+$labels['itipmessagesingleoccurrence'] = 'The message only refers to this single occurrence';
+$labels['itipmessagefutureoccurrence'] = 'The message refers to this and all future occurrences';
$labels['youhaveaccepted'] = 'You have accepted this invitation';
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
commit 108fae9dd038c9756f6edfaff7ed9f39e4200d7d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Sun Feb 15 18:27:38 2015 +0100
Correctly save 'this-and-future' replies; remove some internal properties before saving (to cache)
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index d4a3436..e414b87 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -899,12 +899,16 @@ class kolab_driver extends calendar_driver
else if ($old['recurrence']['EXCEPTIONS'])
$event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
+ // remove some internal properties which should not be saved
+ unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'],
+ $event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']);
+
switch ($savemode) {
case 'new':
// save submitted data as new (non-recurring) event
$event['recurrence'] = array();
$event['uid'] = $this->cal->generate_uid();
- unset($event['recurrence_id'], $event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
+ unset($event['recurrence_id'], $event['_instance'], $event['id']);
// copy attachment data to new event
foreach ((array)$event['attachments'] as $idx => $attachment) {
@@ -921,16 +925,13 @@ class kolab_driver extends calendar_driver
// recurring instances shall not store recurrence rules and attachments
$event['recurrence'] = array();
$event['thisandfuture'] = $savemode == 'future';
- unset($event['attachments']);
+ unset($event['attachments'], $event['id']);
// increment sequence of this instance if scheduling is affected
if ($reschedule) {
$event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
}
- // remove some internal properties which should not be saved
- unset($event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
-
// save properties to a recurrence exception instance
if ($old['recurrence_id'] && is_array($master['recurrence']['EXCEPTIONS'])) {
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index c3bf625..63a0548 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1414,7 +1414,7 @@ class libcalendaring extends rcube_plugin
if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
$recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
$object['_instance'] = $object['recurrence_date']->format($recurrence_id_format);
- $object['_savemode'] = $event['thisandfuture'] ? 'future' : 'current';
+ $object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current';
}
else if (!empty($object['recurrence_id']) || !empty($object['_instance'])) {
if (strlen($object['_instance']) > 4) {
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index fe10f9d..a5b0f73 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -103,7 +103,6 @@ class kolab_format_event extends kolab_format_xcal
foreach((array)$object['recurrence']['EXCEPTIONS'] as $i => $exception) {
$exevent = new kolab_format_event;
$exevent->set(($compacted = $this->compact_exception($exception, $object))); // only save differing values
- console('COMPACTED', $compacted);
// get value for recurrence-id
if (!empty($exception['recurrence_date']) && is_a($exception['recurrence_date'], 'DateTime')) {
commit 26381f82a775da9f8876fbcd6a189224d70e12df
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Sun Feb 15 17:21:30 2015 +0100
Send iTip notifications for main event of savemode is 'all'
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 880086d..f61b681 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -846,9 +846,16 @@ class calendar extends rcube_plugin
$event['_notify'] = 1;
// read old event data in order to find changes
- if (($event['_notify'] || $event['_decline']) && $action != 'new')
+ if (($event['_notify'] || $event['_decline']) && $action != 'new') {
$old = $this->driver->get_event($event);
+ // load main event when savemode is 'all'
+ if ($event['_savemode'] == 'all' && $old['recurrence_id']) {
+ $old['id'] = $old['recurrence_id'];
+ $old = $this->driver->get_event($old);
+ }
+ }
+
switch ($action) {
case "new":
// create UID for new event
@@ -1128,9 +1135,17 @@ class calendar extends rcube_plugin
// 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);
+ // 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);
+ }
+
// 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']);
commit d564e23aa345ce67c7c8cabca1a324628a94fcba
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Sun Feb 15 17:10:22 2015 +0100
Use the right list of properties relevenat for scheduling (follow-up of commit 12591358). Static vars don't work here as intended
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 815f51e..d4a3436 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1041,7 +1041,8 @@ class kolab_driver extends calendar_driver
}
// iterate through the list of properties considered 'significant' for scheduling
- $reschedule = kolab_format_event::check_rescheduling($event, $old);
+ $kolab_event = $old['_formatobj'] ?: new kolab_format_event();
+ $reschedule = $kolab_event->check_rescheduling($event, $old);
// reset all attendee status to needs-action (#4360)
if ($update && $reschedule && is_array($event['attendees'])) {
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index f3c52df..fe10f9d 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -44,6 +44,9 @@ class kolab_format_event extends kolab_format_xcal
$this->obj = $data;
$this->loaded = true;
}
+
+ // copy static property overriden by this class
+ $this->_scheduling_properties = self::$scheduling_properties;
}
/**
@@ -275,13 +278,4 @@ class kolab_format_event extends kolab_format_xcal
return $exception;
}
- /**
- * Identify changes considered relevant for scheduling
- *
- * @see kolab_format_xcal::check_rescheduling()
- */
- public static function check_rescheduling($object, $old, $checks = null)
- {
- return parent::check_rescheduling($object, $old, $checks ?: self::$scheduling_properties);
- }
}
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index 2c0cda5..d3ddfe9 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -32,6 +32,16 @@ class kolab_format_task extends kolab_format_xcal
protected $read_func = 'readTodo';
protected $write_func = 'writeTodo';
+ /**
+ * Default constructor
+ */
+ function __construct($data = null, $version = 3.0)
+ {
+ parent::__construct(is_string($data) ? $data : null, $version);
+
+ // copy static property overriden by this class
+ $this->_scheduling_properties = self::$scheduling_properties;
+ }
/**
* Set properties to the kolabformat object
@@ -127,13 +137,4 @@ class kolab_format_task extends kolab_format_xcal
return $tags;
}
- /**
- * Identify changes considered relevant for scheduling
- *
- * @see kolab_format_xcal::check_rescheduling()
- */
- public static function check_rescheduling($object, $old, $checks = null)
- {
- return parent::check_rescheduling($object, $old, $checks ?: self::$scheduling_properties);
- }
}
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 8d751a6..6d49ad1 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -32,6 +32,8 @@ abstract class kolab_format_xcal extends kolab_format
public static $scheduling_properties = array('start', 'end', 'location');
+ protected $_scheduling_properties = null;
+
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
@@ -317,11 +319,10 @@ abstract class kolab_format_xcal extends kolab_format
}
else {
$object['sequence'] = $old_sequence;
- $old = $this->data['uid'] ? $this->data : $this->to_array();
// increment sequence when updating properties relevant for scheduling.
// RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
- if (self::check_rescheduling($object, $old)) {
+ if ($this->check_rescheduling($object)) {
$object['sequence']++;
}
}
@@ -634,15 +635,18 @@ abstract class kolab_format_xcal extends kolab_format
*
* @param array Hash array with NEW object properties
* @param array Hash array with OLD object properties
- * @param array List of object properties to check for changes
*
* @return boolean True if changes affect scheduling, False otherwise
*/
- public static function check_rescheduling($object, $old, $checks = null)
+ public function check_rescheduling($object, $old = null)
{
$reschedule = false;
- foreach ($checks ?: self::$scheduling_properties as $prop) {
+ 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) {
commit 12591358e600dd3b0ad91a8f29345354e34000ac
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Sun Feb 15 16:33:39 2015 +0100
Consider a change in recurrence rule significant for rescheduling (#4366)
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index f1e66ec..880086d 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -993,7 +993,7 @@ class calendar extends rcube_plugin
if ($success = $this->driver->edit_rsvp($event, $status)) {
$noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);
$noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
- $reload = $event['calendar'] != $ev['calendar'] ? 2 : 1;
+ $reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1;
$organizer = null;
$emails = $this->get_user_emails();
@@ -1131,8 +1131,6 @@ class calendar extends rcube_plugin
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
- // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
-
// only notify if data really changed (TODO: do diff check on client already)
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
$sent = $this->notify_attendees($event, $old, $action, $event['_comment']);
@@ -1990,6 +1988,8 @@ class calendar extends rcube_plugin
$sent = -100;
}
+ // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
+
// send CANCEL message to removed attendees
foreach ((array)$old['attendees'] as $attendee) {
if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current))
@@ -2215,7 +2215,7 @@ class calendar extends rcube_plugin
public static function event_diff($a, $b)
{
$diff = array();
- $ignore = array('changed' => 1, 'attachments' => 1, 'recurrence' => 1, '_notify' => 1, '_owner' => 1);
+ $ignore = array('changed' => 1, 'attachments' => 1, '_notify' => 1, '_owner' => 1, '_savemode' => 1);
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
if (!$ignore[$key] && $a[$key] != $b[$key])
$diff[] = $key;
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 4b450f8..815f51e 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1035,26 +1035,13 @@ class kolab_driver extends calendar_driver
*/
public function check_scheduling(&$event, $old, $update = true)
{
- $reschedule = false;
-
// skip this check when importing iCal/iTip events
if (isset($event['sequence']) || !empty($event['_method'])) {
- return $reschedule;
+ return false;
}
// iterate through the list of properties considered 'significant' for scheduling
- foreach (kolab_format_event::$scheduling_properties as $prop) {
- $a = $old[$prop];
- $b = $event[$prop];
- if ($event['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
- $a = $a->format('Y-m-d');
- $b = $b->format('Y-m-d');
- }
- if ($a != $b) {
- $reschedule = true;
- break;
- }
- }
+ $reschedule = kolab_format_event::check_rescheduling($event, $old);
// reset all attendee status to needs-action (#4360)
if ($update && $reschedule && is_array($event['attendees'])) {
diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist
index 6e4b613..3a8476c 100644
--- a/plugins/libkolab/config.inc.php.dist
+++ b/plugins/libkolab/config.inc.php.dist
@@ -41,7 +41,7 @@ $config['kolab_messages_cache_bypass'] = 0;
// These event properties contribute to a significant revision to the calendar component
// and if changed will increment the sequence number relevant for scheduling according to RFC 5545
-$config['kolab_event_scheduling_properties'] = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+$config['kolab_event_scheduling_properties'] = array('start', 'end', 'allday', 'recurrence', 'location', 'status', 'cancelled');
// These task properties contribute to a significant revision to the calendar component
// and if changed will increment the sequence number relevant for scheduling according to RFC 5545
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index bf17149..f3c52df 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -26,7 +26,7 @@ class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
- public static $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+ public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'status', 'cancelled');
protected $objclass = 'Event';
protected $read_func = 'readEvent';
@@ -100,6 +100,7 @@ class kolab_format_event extends kolab_format_xcal
foreach((array)$object['recurrence']['EXCEPTIONS'] as $i => $exception) {
$exevent = new kolab_format_event;
$exevent->set(($compacted = $this->compact_exception($exception, $object))); // only save differing values
+ console('COMPACTED', $compacted);
// get value for recurrence-id
if (!empty($exception['recurrence_date']) && is_a($exception['recurrence_date'], 'DateTime')) {
@@ -274,4 +275,13 @@ class kolab_format_event extends kolab_format_xcal
return $exception;
}
+ /**
+ * Identify changes considered relevant for scheduling
+ *
+ * @see kolab_format_xcal::check_rescheduling()
+ */
+ public static function check_rescheduling($object, $old, $checks = null)
+ {
+ return parent::check_rescheduling($object, $old, $checks ?: self::$scheduling_properties);
+ }
}
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index ee0ca6a..2c0cda5 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -126,4 +126,14 @@ class kolab_format_task extends kolab_format_xcal
return $tags;
}
+
+ /**
+ * Identify changes considered relevant for scheduling
+ *
+ * @see kolab_format_xcal::check_rescheduling()
+ */
+ public static function check_rescheduling($object, $old, $checks = null)
+ {
+ return parent::check_rescheduling($object, $old, $checks ?: self::$scheduling_properties);
+ }
}
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index d0f89b6..8d751a6 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -321,17 +321,8 @@ abstract class kolab_format_xcal extends kolab_format
// increment sequence when updating properties relevant for scheduling.
// RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
- foreach (self::$scheduling_properties as $prop) {
- $a = $old[$prop];
- $b = $object[$prop];
- if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
- $a = $a->format('Y-m-d');
- $b = $b->format('Y-m-d');
- }
- if ($a != $b) {
- $object['sequence']++;
- break;
- }
+ if (self::check_rescheduling($object, $old)) {
+ $object['sequence']++;
}
}
}
@@ -637,4 +628,39 @@ abstract class kolab_format_xcal extends kolab_format
return $tags;
}
+
+ /**
+ * Identify changes considered relevant for scheduling
+ *
+ * @param array Hash array with NEW object properties
+ * @param array Hash array with OLD object properties
+ * @param array List of object properties to check for changes
+ *
+ * @return boolean True if changes affect scheduling, False otherwise
+ */
+ public static function check_rescheduling($object, $old, $checks = null)
+ {
+ $reschedule = false;
+
+ foreach ($checks ?: 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') {
+ unset($a['EXCEPTIONS']);
+ unset($b['EXCEPTIONS']);
+ $a = array_filter($a);
+ $b = array_filter($b);
+ }
+ if ($a != $b) {
+ $reschedule = true;
+ break;
+ }
+ }
+
+ return $reschedule;
+ }
}
\ No newline at end of file
commit f09948eefe6b7b1d275bc6d5465c02e8c0dbe9c2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Sun Feb 15 15:12:08 2015 +0100
Disable recurrence and attachments forms when editing a single recurrence instance. These properties cannot be stored in recurrence exceptions
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 1f9f399..f9e0d28 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -691,6 +691,8 @@ function rcube_calendar_ui(settings)
// reset dialog first
$('#eventtabs').get(0).reset();
+ $('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', false);
+ $('#event-panel-recurrence, #event-panel-attachments').removeClass('disabled');
// allow other plugins to do actions when event form is opened
rcmail.triggerEvent('calendar-event-init', {o: event});
@@ -752,7 +754,7 @@ function rcube_calendar_ui(settings)
if (event.id && event.recurrence) {
var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
$('#edit-recurring-warning').show();
- $('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true);
+ $('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true).change();
}
else
$('#edit-recurring-warning').hide();
@@ -803,7 +805,7 @@ function rcube_calendar_ui(settings)
// attachments
var load_attachments_tab = function()
{
- rcmail.enable_command('remove-attachment', !calendar.readonly);
+ rcmail.enable_command('remove-attachment', !calendar.readonly && !event.recurrence_id);
rcmail.env.deleted_attachments = [];
// we're sharing some code for uploads handling with app.js
rcmail.env.attachments = [];
@@ -4163,6 +4165,13 @@ function rcube_calendar_ui(settings)
event_rsvp($(this).attr('rel'))
});
+ $('#eventedit input.edit-recurring-savemode').change(function(e) {
+ var sel = $('input.edit-recurring-savemode:checked').val(),
+ disabled = sel == 'current' || sel == 'future';
+ $('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', disabled);
+ $('#event-panel-recurrence, #event-panel-attachments')[(disabled?'addClass':'removeClass')]('disabled');
+ })
+
$('#eventshow .changersvp').click(function(e) {
var d = $('#eventshow'),
h = -$(this).closest('.event-line').toggle().height();
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 1e3f0ea..4b450f8 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -918,9 +918,10 @@ class kolab_driver extends calendar_driver
case 'future':
case 'current':
- // recurring instances shall not store recurrence rules
+ // recurring instances shall not store recurrence rules and attachments
$event['recurrence'] = array();
$event['thisandfuture'] = $savemode == 'future';
+ unset($event['attachments']);
// increment sequence of this instance if scheduling is affected
if ($reschedule) {
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 1c2eca5..0fec69c 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -676,6 +676,10 @@ a.miniColors-trigger {
outline: none;
}
+#event-panel-attachments.disabled .attachmentslist li a.delete {
+ visibility: hidden;
+}
+
.event-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
commit 78622133a94c444b9c2c205649a564c37739f4f9
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Sun Feb 15 14:32:31 2015 +0100
Reliably identify recurrence instances throughout the application to support invitations of recurring events (#4387)
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 51381cb..f1e66ec 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -841,15 +841,12 @@ class calendar extends rcube_plugin
$event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true);
$success = $reload = $got_msg = false;
- // don't notify if modifying a recurring instance (really?)
- if ($event['_savemode'] && in_array($event['_savemode'], array('current','future')) && $event['_notify'] && $action != 'remove')
- unset($event['_notify']);
// force notify if hidden + active
- else if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1)
+ if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1)
$event['_notify'] = 1;
// read old event data in order to find changes
- if (($event['_notify'] || $event['decline']) && $action != 'new')
+ if (($event['_notify'] || $event['_decline']) && $action != 'new')
$old = $this->driver->get_event($event);
switch ($action) {
@@ -928,8 +925,14 @@ class calendar extends rcube_plugin
$got_msg = true;
}
+ // send cancellation for the main event
+ if ($event['_savemode'] == 'all')
+ unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']);
+ else if ($event['_savemode'] == 'future')
+ $old['thisandfuture'] = true;
+
// send iTIP reply that participant has declined the event
- if ($success && $event['decline']) {
+ if ($success && $event['_decline']) {
$emails = $this->get_user_emails();
foreach ($old['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
@@ -939,7 +942,7 @@ class calendar extends rcube_plugin
$reply_sender = $attendee['email'];
}
}
-
+
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
@@ -974,6 +977,7 @@ class calendar extends rcube_plugin
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$ev['free_busy'] = $event['free_busy'];
+ $ev['_savemode'] = $event['_savemode'];
// send invitation to delegatee + add it as attendee
if ($status == 'delegated' && $event['to']) {
@@ -1127,11 +1131,7 @@ class calendar extends rcube_plugin
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
- // sending notification on a recurrence instance -> re-send the main event
- if ($event['recurrence_id']) {
- $event = $this->driver->get_event(array('id' => $event['recurrence_id'], 'cal' => $event['calendar']));
- $action = 'edit';
- }
+ // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
// only notify if data really changed (TODO: do diff check on client already)
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
@@ -1945,6 +1945,9 @@ class calendar extends rcube_plugin
// add comment to the iTip attachment
$event['comment'] = $comment;
+ // set a valid recurrence-id if this is a recurrence instance
+ libcalendaring::identify_recurrence_instance($event);
+
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
$message = $itip->compose_itip_message($event, $method, $event['sequence'] > $old['sequence']);
@@ -2405,9 +2408,10 @@ class calendar extends rcube_plugin
{
$success = false;
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
+ $inst = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
// search for event if only UID is given
- if ($event = $this->driver->get_event(array('uid' => $uid), true)) {
+ if ($event = $this->driver->get_event(array('uid' => $uid, '_instance' => $inst), true)) {
$success = $this->driver->remove_event($event, true);
}
@@ -2722,12 +2726,13 @@ class calendar extends rcube_plugin
// save to calendar
if ($calendar && !$calendar['readonly']) {
- $event['calendar'] = $calendar['id'];
-
- // check for existing event with the same UID
- $existing = $this->driver->get_event($event['uid'], true, false, true);
-
+ // check for existing event with the same UID
+ $existing = $this->driver->get_event($event, true, false, true);
+
if ($existing) {
+ // forward savemode for correct updates of recurring events
+ $existing['_savemode'] = $event['_savemode'];
+
// only update attendee status
if ($event['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address
@@ -2829,6 +2834,8 @@ class calendar extends rcube_plugin
if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
$event['free_busy'] = 'free';
}
+ // save to the selected/default calendar
+ $event['calendar'] = $calendar['id'];
$success = $this->driver->new_event($event);
}
else if ($status == 'declined')
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index c1e3d55..1f9f399 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -554,6 +554,14 @@ function rcube_calendar_ui(settings)
$('#event-rsvp a.reply-comment-toggle').show();
$('#event-rsvp .itip-reply-comment textarea').hide().val('');
+
+ if (event.recurrence && event.id) {
+ var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
+ $('#event-rsvp input.rsvp-replymode[value="'+sel+'"]').prop('checked', true);
+ $('#event-rsvp .rsvp-replymode-message').show();
+ }
+ else
+ $('#event-rsvp .rsvp-replymode-message').hide();
}
var buttons = [];
@@ -742,11 +750,9 @@ function rcube_calendar_ui(settings)
// show warning if editing a recurring event
if (event.id && event.recurrence) {
- var allow_exceptions = !has_attendees(event) || !is_organizer(event),
- sel = event._savemode || (allow_exceptions && event.thisandfuture ? 'future' : (allow_exceptions && event.isexception ? 'current' : 'all'));
+ var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
$('#edit-recurring-warning').show();
$('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true);
- $('input.edit-recurring-savemode[value="current"], input.edit-recurring-savemode[value="future"]').prop('disabled', !allow_exceptions);
}
else
$('#edit-recurring-warning').hide();
@@ -2411,7 +2417,7 @@ function rcube_calendar_ui(settings)
}
// submit status change to server
- var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val() }, (delegate || {})),
+ var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val(), _savemode: $('input.rsvp-replymode:checked').val() }, (delegate || {})),
noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
// import event from mail (temporary iTip event)
@@ -2425,7 +2431,8 @@ function rcube_calendar_ui(settings)
_to: (delegate ? delegate.to : null),
_rsvp: (delegate && delegate.rsvp) ? 1 : 0,
_noreply: noreply,
- _comment: submit_data.comment
+ _comment: submit_data.comment,
+ _savemode: submit_data._savemode
});
}
else if (settings.invitation_calendars) {
@@ -2501,7 +2508,7 @@ function rcube_calendar_ui(settings)
// mark all recurring instances as temp
if (event.recurrence || event.recurrence_id) {
- var base_id = event.recurrence_id ? event.recurrence_id.replace(/-\d+$/, '') : event.id;
+ var base_id = event.recurrence_id ? event.recurrence_id.replace(/-\d+(T\d{6})?$/, '') : event.id;
$.each(fc.fullCalendar('clientEvents', function(e){ return e.id == base_id || e.recurrence_id == base_id; }), function(i,ev) {
ev.temp = true;
ev.editable = false;
@@ -2566,7 +2573,7 @@ function rcube_calendar_ui(settings)
// recurring event: user needs to select the savemode
if (event.recurrence) {
var disabled_state = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
-
+/*
if (_has_attendees) {
if (action == 'remove') {
if (!_is_organizer) {
@@ -2578,7 +2585,7 @@ function rcube_calendar_ui(settings)
disabled_state = ' disabled';
}
}
-
+*/
html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
rcmail.gettext(message_label, 'calendar') + '</div>' +
'<div class="savemode">' +
@@ -2606,8 +2613,10 @@ function rcube_calendar_ui(settings)
else {
if ($dialog.find('input.confirm-attendees-donotify').length)
data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
- if (decline && $dialog.find('input.confirm-attendees-decline:checked').length)
- data.decline = 1;
+ if (decline) {
+ data._decline = $dialog.find('input.confirm-attendees-decline:checked').length;
+ data._notify = 0;
+ }
update_event(action, data);
}
@@ -2622,7 +2631,7 @@ function rcube_calendar_ui(settings)
text: rcmail.gettext((action == 'remove' ? 'delete' : 'save'), 'calendar'),
click: function() {
data._notify = notify && $dialog.find('input.confirm-attendees-donotify:checked').length ? 1 : 0;
- data.decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
+ data._decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
update_event(action, data);
$(this).dialog("close");
}
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 24e7a2e..e402db9 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -50,6 +50,7 @@
* 'EXCEPTIONS' => array(<event>), list of event objects which denote exceptions in the recurrence chain
* ),
* 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event
+ * '_instance' => 'ID of the recurring instance', // identifies an instance within a recurrence chain
* 'categories' => 'Event category',
* 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as
* 'status' => 'TENTATIVE|CONFIRMED|CANCELLED', // event status according to RFC 2445
@@ -469,7 +470,6 @@ abstract class calendar_driver
if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
$next_event['id'] = $next_event['uid'];
$next_event['recurrence_id'] = $event['uid'];
- $next_event['_instance'] = $i;
$events[] = $next_event;
}
else if ($next_event['start'] > $end) { // stop loop if out of range
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 2b50ac8..15f030d 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -661,7 +661,7 @@ class kolab_calendar extends kolab_storage_folder_api
*/
private function _merge_event_data(&$event, $overlay)
{
- static $forbidden = array('id','uid','created','changed','recurrence','organizer','attendees','sequence');
+ static $forbidden = array('id','uid','recurrence','organizer','_attachments');
foreach ($overlay as $prop => $value) {
// adjust time of the recurring event instance
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 98318f2..1e3f0ea 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -534,8 +534,13 @@ class kolab_driver extends calendar_driver
public function get_event($event, $writeable = false, $active = false, $personal = false)
{
if (is_array($event)) {
- $id = $event['id'] ? $event['id'] : $event['uid'];
+ $id = $event['id'] ?: $event['uid'];
$cal = $event['calendar'];
+
+ // we're looking for a recurring instance: expand the ID to our internal convention for recurring instanced
+ if (!$event['id'] && $event['_instance']) {
+ $id .= '-' . $event['_instance'];
+ }
}
else {
$id = $event;
@@ -687,11 +692,11 @@ class kolab_driver extends calendar_driver
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id']) {
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
- $savemode = $event['_savemode'];
+ $savemode = $event['_savemode'] ?: ($event['_instance'] ? 'current' : 'all');
}
// removing an exception instance
- if ($event['recurrence_id']) {
+ if ($event['recurrence_id'] && $master['recurrence'] && is_array($master['recurrence']['EXCEPTIONS'])) {
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
if ($exception['_instance'] == $event['_instance']) {
unset($master['recurrence']['EXCEPTIONS'][$i]);
@@ -880,7 +885,7 @@ class kolab_driver extends calendar_driver
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id']) {
$master = $old['recurrence_id'] ? $fromcalendar->get_event($old['recurrence_id']) : $old;
- $savemode = $event['_savemode'];
+ $savemode = $event['_savemode'] ?: ($old['recurrence_id'] ? 'current' : 'all');
}
// check if update affects scheduling and update attendee status accordingly
@@ -919,7 +924,7 @@ class kolab_driver extends calendar_driver
// increment sequence of this instance if scheduling is affected
if ($reschedule) {
- $event['sequence'] = $old['sequence'] + 1;
+ $event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
}
// remove some internal properties which should not be saved
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index fae98bb..d3af94d 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -67,7 +67,6 @@ class calendar_recurrence extends libcalendaring_recurrence
{
if ($next_start = $this->next()) {
$next = $this->event;
- $next['recurrence_id'] = $next_start->format('Y-m-d');
$next['start'] = $next_start;
if ($this->duration) {
@@ -75,6 +74,10 @@ class calendar_recurrence extends libcalendaring_recurrence
$next['end']->add($this->duration);
}
+ $recurrence_id_format = $next['allday'] ? 'Ymd' : 'Ymd\THis';
+ $next['recurrence_date'] = clone $next_start;
+ $next['_instance'] = $next_start->format($recurrence_id_format);
+
unset($next['_formatobj']);
return $next;
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 50f8b64..1c2eca5 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1059,6 +1059,26 @@ td.topalign {
text-align: center;
}
+.event-dialog-message .rsvp-replymode-message {
+ margin-top: 0.8em;
+ margin-bottom: 0.6em;
+}
+
+.event-dialog-message .rsvp-replymode-message .replymode-select {
+ padding-left: 22px;
+}
+
+.event-dialog-message .rsvp-replymode-message label {
+ color: inherit;
+ margin-right: 0.4em;
+ white-space: nowrap;
+ min-width: 4em;
+}
+
+.event-dialog-message .rsvp-replymode-message input.rsvp-replymode {
+ margin-right: 0.4em;
+}
+
#event-rsvp,
#edit-attendees-notify {
margin: 0.6em 0 0.3em 0;
@@ -2159,6 +2179,15 @@ div.calendar-invitebox td.sensitivity {
font-weight: bold;
}
+div.calendar-invitebox td.recurrence-id {
+ text-transform: uppercase;
+ font-style: italic;
+}
+
+div.calendar-invitebox td em {
+ font-weight: bold;
+}
+
#event-rsvp .rsvp-buttons,
div.calendar-invitebox .itip-buttons div {
margin-top: 0.5em;
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 3eead6f..53284a4 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -98,8 +98,10 @@ class libcalendaring_itip
if (!$this->sender['name'])
$this->sender['name'] = $this->sender['email'];
- if (!$message)
+ if (!$message) {
+ libcalendaring::identify_recurrence_instance($event);
$message = $this->compose_itip_message($event, $method, $rsvp);
+ }
$mailto = rcube_idn_to_ascii($recipient['email']);
@@ -121,12 +123,19 @@ class libcalendaring_itip
($attendee['name'] ? $attendee['name'] : $attendee['email']);
}
+ $recurrence_info = '';
+ if (!empty($event['recurrence_id'])) {
+ $recurrence_info = "\n\n** " . $this->gettext('itip'.strtolower($method).'occurrenceonly') . ' **';
+ }
+ else if (!empty($event['recurrence'])) {
+ $recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']));
+ }
+
$mailbody = $this->gettext(array(
'name' => $bodytext,
'vars' => array(
'title' => $event['title'],
- 'date' => $this->lib->event_date_text($event, true) .
- (empty($event['recurrence']) ? '' : sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']))),
+ 'date' => $this->lib->event_date_text($event, true) . $recurrence_info,
'attendees' => join(",\n ", $attendees_list),
'sender' => $this->sender['name'],
'organizer' => $this->sender['name'],
@@ -151,6 +160,10 @@ class libcalendaring_itip
$message->headers($headers, true);
$message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
+ if ($this->rc->config->get('libcalendaring_itip_debug', false)) {
+ console('iTip ' . $method, $message->txtHeaders() . "\n\r" . $message->get());
+ }
+
// finally send the message
$this->itip_send = true;
$sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
@@ -230,6 +243,9 @@ class libcalendaring_itip
array_unshift($reply_attendees, $replying_attendee);
$event['attendees'] = $reply_attendees;
}
+ if ($event['recurrence']) {
+ unset($event['recurrence']['EXCEPTIONS']);
+ }
}
// set RSVP for every attendee
else if ($method == 'REQUEST') {
@@ -239,6 +255,11 @@ class libcalendaring_itip
}
}
}
+ else if ($method == 'CANCEL') {
+ if ($event['recurrence']) {
+ unset($event['recurrence']['EXCEPTIONS']);
+ }
+ }
// compose multipart message using PEAR:Mail_Mime
$message = new Mail_mime("\r\n");
@@ -453,6 +474,7 @@ class libcalendaring_itip
$changed = is_object($event['changed']) ? $event['changed'] : $message_date;
$metadata = array(
'uid' => $event['uid'],
+ '_instance' => $event['_instance'],
'changed' => $changed ? $changed->format('U') : 0,
'sequence' => intval($event['sequence']),
'method' => $method,
@@ -580,12 +602,13 @@ class libcalendaring_itip
// for CANCEL messages, we can:
else if ($method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
+ $event_prop = array_filter(array('uid' => $event['uid'], '_instance' => $event['_instance']));
// 1. remove the event from our calendar
$button_remove = html::tag('input', array(
'type' => 'button',
'class' => 'button',
- 'onclick' => "rcube_libcalendaring.remove_from_itip('" . JQ($event['uid']) . "', '$task', '" . JQ($event['title']) . "')",
+ 'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . JQ($event['title']) . "')",
'value' => $this->gettext('removefromcalendar'),
));
@@ -646,8 +669,6 @@ class libcalendaring_itip
));
}
- $buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']));
-
// add localized texts for the delegation dialog
if (in_array('delegated', $actions)) {
foreach (array('itipdelegated','itipcomment','delegateinvitation',
@@ -656,9 +677,23 @@ class libcalendaring_itip
}
}
+ $savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode'));
+
return html::div($attrib,
html::div('label', $this->gettext('acceptinvitation')) .
- html::div('rsvp-buttons', $buttons));
+ html::div('rsvp-buttons',
+ $buttons .
+ html::div(array('class' => 'rsvp-replymode-message', 'style' => 'display:none'),
+ html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->gettext('rsvprecurringevent')) .
+ html::div('replymode-select',
+ html::label(null, $savemode_radio->show('all', array('value' => 'all')) . $this->gettext('allevents')) .
+ html::label(null, $savemode_radio->show(null, array('value' => 'current')) . $this->gettext('currentevent')) .
+ html::label(null, $savemode_radio->show(null, array('value' => 'future')) . $this->gettext('futurevents'))
+ )
+ ) .
+ html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
+ )
+ );
}
/**
@@ -705,7 +740,11 @@ class libcalendaring_itip
$table->add('label', $this->gettext('date'));
$table->add('date', Q($this->lib->event_date_text($event)));
}
- if (!empty($event['recurrence'])) {
+ if (!empty($event['recurrence_date'])) {
+ $table->add('label', '');
+ $table->add('recurrence-id', $this->gettext('itipsingleoccurrence'));
+ }
+ else if (!empty($event['recurrence'])) {
$table->add('label', $this->gettext('recurring'));
$table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
}
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 0a2949d..a13ebf7 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -961,11 +961,11 @@ rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
/**
*
*/
-rcube_libcalendaring.remove_from_itip = function(uid, task, title)
+rcube_libcalendaring.remove_from_itip = function(event, task, title)
{
if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
rcmail.http_post(task + '/itip-remove',
- { uid: uid },
+ event,
rcmail.set_busy(true, 'itip.savingdata')
);
}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index f1c4514..c3bf625 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1310,6 +1310,9 @@ class libcalendaring extends rcube_plugin
$charset = $part->ctype_parameters['charset'] ?: RCMAIL_CHARSET;
$this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
+ // check if the parsed object is an instance of a recurring event/task
+ array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
+
// stop on the part that has an iTip method specified
if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
$this->mail_ical_parser->message_date = $this->ical_message->headers->date;
@@ -1374,6 +1377,9 @@ class libcalendaring extends rcube_plugin
$object['_sender'] = preg_match(self::$email_regex, $headers->from, $m) ? $m[1] : '';
$object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
+ // check if this is an instance of a recurring event/task
+ self::identify_recurrence_instance($object);
+
return $object;
}
@@ -1395,6 +1401,30 @@ class libcalendaring extends rcube_plugin
);
}
+ /**
+ * Single occourrences of recurring events are identified by their RECURRENCE-ID property
+ * in iCal which is represented as 'recurrence_date' in our internal data structure.
+ *
+ * Check if such a property exists and derive the '_instance' identifier and '_savemode'
+ * attributes which are used in the storage backend to identify the nested exception item.
+ */
+ public static function identify_recurrence_instance(&$object)
+ {
+ // set instance and 'savemode' according to recurrence-id
+ if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
+ $recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
+ $object['_instance'] = $object['recurrence_date']->format($recurrence_id_format);
+ $object['_savemode'] = $event['thisandfuture'] ? 'future' : 'current';
+ }
+ else if (!empty($object['recurrence_id']) || !empty($object['_instance'])) {
+ if (strlen($object['_instance']) > 4) {
+ $object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
+ }
+ else {
+ $object['recurrence_date'] = clone $object['start'];
+ }
+ }
+ }
/********* Attendee handling functions *********/
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 4163cfb..10c2223 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -948,6 +948,13 @@ class libvcalendar implements Iterator
if (!empty($event['due']))
$ve->add($this->datetime_prop('DUE', $event['due'], false));
+ // we're exporting a recurrence instance only
+ if (!$recurrence_id && $event['recurrence_date'] && $event['recurrence_date'] instanceof DateTime) {
+ $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $event['recurrence_date'], false, (bool)$event['allday']);
+ if ($event['thisandfuture'])
+ $recurrence_id->add('RANGE', 'THISANDFUTURE');
+ }
+
if ($recurrence_id)
$ve->add($recurrence_id);
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 3e49838..31f08fd 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -108,6 +108,12 @@ $labels['acceptinvitation'] = 'Do you accept this invitation?';
$labels['acceptattendee'] = 'Accept participant';
$labels['declineattendee'] = 'Decline participant';
$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
+$labels['rsvprecurringevent'] = 'This is a series of events! Does your response apply to all, this occurrence only or this and future occurrences?';
+
+$labels['itipsingleoccurrence'] = 'This is a <em>single occurrence</em> out of a series of events';
+$labels['itiprequestoccurrenceonly'] = 'The invitation only refers to this single occurrence';
+$labels['itipreplyoccurrenceonly'] = 'The response only refers to this single occurrence';
+$labels['itipcanceloccurrenceonly'] = 'The cancellation only refers to this single occurrence';
$labels['youhaveaccepted'] = 'You have accepted this invitation';
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
diff --git a/plugins/libkolab/lib/kolab_date_recurrence.php b/plugins/libkolab/lib/kolab_date_recurrence.php
index 06dd331..b2511f2 100644
--- a/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -87,9 +87,13 @@ class kolab_date_recurrence
$next_end->add($this->duration);
$next = $this->object->to_array();
- $next['recurrence_id'] = $next_start->format('Y-m-d');
$next['start'] = $next_start;
$next['end'] = $next_end;
+
+ $recurrence_id_format = $next['allday'] ? 'Ymd' : 'Ymd\THis';
+ $next['recurrence_date'] = clone $next_start;
+ $next['_instance'] = $next_start->format($recurrence_id_format);
+
unset($next['_formatobj']);
return $next;
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 075c517..bf17149 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -237,6 +237,7 @@ class kolab_format_event extends kolab_format_xcal
private function compact_exception($exception, $master)
{
$forbidden = array('recurrence','organizer','_attachments');
+ $whitelist = array('start','end');
foreach ($forbidden as $prop) {
if (array_key_exists($prop, $exception)) {
@@ -245,7 +246,7 @@ class kolab_format_event extends kolab_format_xcal
}
foreach ($master as $prop => $value) {
- if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value) {
+ if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value && !in_array($prop, $whitelist)) {
unset($exception[$prop]);
}
}
More information about the commits
mailing list