summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2015-02-17 10:36:01 (GMT)
committerThomas Bruederli <bruederli@kolabsys.com>2015-02-17 10:36:01 (GMT)
commit8a90069071821f4cf2cdac6d7fce82cf12a32c11 (patch)
tree4ced716d7976ee9f9edd8cad8171ccdb69b4bf38 /plugins
parent7fd2eb873dbeed5fb955e459b20e1134c09892b0 (diff)
downloadroundcubemail-plugins-kolab-8a90069071821f4cf2cdac6d7fce82cf12a32c11.tar.gz
- 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)
Diffstat (limited to 'plugins')
-rw-r--r--plugins/calendar/calendar.php27
-rw-r--r--plugins/calendar/drivers/calendar_driver.php12
-rw-r--r--plugins/calendar/drivers/kolab/kolab_calendar.php71
-rw-r--r--plugins/calendar/drivers/kolab/kolab_driver.php182
-rw-r--r--plugins/libcalendaring/libcalendaring.php6
-rw-r--r--plugins/libcalendaring/localization/en_US.inc2
6 files changed, 239 insertions, 61 deletions
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';