plugins/calendar plugins/libcalendaring plugins/libkolab
Thomas Brüderli
bruederli at kolabsys.com
Thu Feb 12 10:14:04 CET 2015
plugins/calendar/drivers/kolab/kolab_calendar.php | 70 ++++++++-------
plugins/calendar/drivers/kolab/kolab_driver.php | 54 ++++++++---
plugins/libcalendaring/libvcalendar.php | 13 +-
plugins/libcalendaring/tests/libvcalendar.php | 1
plugins/libcalendaring/tests/resources/recurrence-id.ics | 2
plugins/libkolab/lib/kolab_format_event.php | 65 ++++++++++---
6 files changed, 138 insertions(+), 67 deletions(-)
New commits:
commit ad55fc706d3f5813f6a0f4b0f93b8970046b4d6d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Feb 12 10:08:22 2015 +0100
Fix handling of Recurrence-ID properties for recurrence exceptions to comply with RFC 5545 (#4385)
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 9d573ee..2b50ac8 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -191,7 +191,7 @@ class kolab_calendar extends kolab_storage_folder_api
// event not found, maybe a recurring instance is requested
if (!$this->events[$id]) {
- $master_id = preg_replace('/-\d+$/', '', $id);
+ $master_id = preg_replace('/-\d+(T\d{6})?$/', '', $id);
if ($master_id != $id && ($record = $this->storage->get_object($master_id)))
$this->events[$master_id] = $this->_to_rcube_event($record);
@@ -455,7 +455,7 @@ class kolab_calendar extends kolab_storage_folder_api
$this->save_links($event['uid'], $links);
$updated = true;
- $this->events[$event['id']] = $this->_to_rcube_event($object);
+ $this->events = array($event['id'] => $this->_to_rcube_event($object));
// refresh local cache with recurring instances
if ($exception_id) {
@@ -564,34 +564,39 @@ class kolab_calendar extends kolab_storage_folder_api
$end->add(new DateInterval($intvl));
}
- // add recurrence exceptions to output
- $i = 0;
+ // copy the recurrence rule from the master event (to be used in the UI)
+ $recurrence_rule = $event['recurrence'];
+ unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
+
+ // read recurrence exceptions first
$events = array();
- $exdates = array();
+ $exdata = array();
$futuredata = array();
- if (is_array($event['recurrence']['EXCEPTIONS'])) {
- // copy the recurrence rule from the master event (to be used in the UI)
- $recurrence_rule = $event['recurrence'];
- unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
+ $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
+ if (is_array($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
+ if (!$exception['_instance'] && is_a($exception['recurrence_date'], 'DateTime'))
+ $exception['_instance'] = $exception['recurrence_date']->format($recurrence_id_format);
+ else if (!$exception['_instance'] && is_a($exception['start'], 'DateTime'))
+ $exception['_instance'] = $exception['start']->format($recurrence_id_format);
+
$rec_event = $this->_to_rcube_event($exception);
- $rec_event['id'] = $event['uid'] . '-' . ++$i;
- $rec_event['recurrence_id'] = $event['uid'];
- $rec_event['recurrence'] = $recurrence_rule;
- $rec_event['_instance'] = $i;
+ $rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
$rec_event['isexception'] = 1;
- $events[] = $rec_event;
// found the specifically requested instance, exiting...
if ($rec_event['id'] == $event_id) {
+ $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 = $rec_event['start']->format('Y-m-d');
- $exdates[$exdate] = $rec_event['id'];
+ $exdate = substr($exception['_instance'], 0, 8);
+ $exdata[$exdate] = $rec_event;
if ($rec_event['thisandfuture']) {
$futuredata[$exdate] = $rec_event;
}
@@ -608,27 +613,27 @@ class kolab_calendar extends kolab_storage_folder_api
$recurrence = new calendar_recurrence($this->cal, $event);
}
+ $i = 0;
while ($next_event = $recurrence->next_instance()) {
- // skip if there's an exception at this date
- $datestr = $next_event['start']->format('Y-m-d');
- if ($exdates[$datestr]) {
- // use this event data for future recurring instances
- if ($futuredata[$datestr])
- $overlay_data = $futuredata[$datestr];
- continue;
- }
+ $datestr = $next_event['start']->format('Ymd');
+ $instance_id = $next_event['start']->format($recurrence_id_format);
+
+ // use this event data for future recurring instances
+ if ($futuredata[$datestr])
+ $overlay_data = $futuredata[$datestr];
// add to output if in range
- $rec_id = $event['uid'] . '-' . ++$i;
+ $rec_id = $event['uid'] . '-' . $instance_id;
if (($next_event['start'] <= $end && $next_event['end'] >= $start) || ($event_id && $rec_id == $event_id)) {
$rec_event = $this->_to_rcube_event($next_event);
+ $rec_event['_instance'] = $instance_id;
- if ($overlay_data) // copy data from a 'this-and-future' exception
- $this->_merge_event_data($rec_event, $overlay_data);
+ if ($overlay_data || $exdata[$datestr]) // copy data from exception
+ $this->_merge_event_data($rec_event, $exdata[$datestr] ?: $overlay_data);
$rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['uid'];
- $rec_event['_instance'] = $i;
+ $rec_event['recurrence'] = $recurrence_rule;
unset($rec_event['_attendees']);
$events[] = $rec_event;
@@ -641,7 +646,7 @@ class kolab_calendar extends kolab_storage_folder_api
break;
// avoid endless recursion loops
- if ($i > 1000)
+ if (++$i > 1000)
break;
}
@@ -661,8 +666,13 @@ class kolab_calendar extends kolab_storage_folder_api
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'))
+ 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;
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index b53ffbf..0d5b9ab 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -692,9 +692,14 @@ class kolab_driver extends calendar_driver
// removing an exception instance
if ($event['recurrence_id']) {
- $i = $event['_instance'] - 1;
- if (!empty($master['recurrence']['EXCEPTIONS'][$i])) {
- unset($master['recurrence']['EXCEPTIONS'][$i]);
+ foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ if ($exception['_instance'] == $event['_instance']) {
+ unset($master['recurrence']['EXCEPTIONS'][$i]);
+ // set event date back to the actual occurrence
+ if ($exception['recurrence_date'])
+ $event['start'] = $exception['recurrence_date'];
+ break;
+ }
}
}
@@ -879,9 +884,11 @@ class kolab_driver extends calendar_driver
}
// keep saved exceptions (not submitted by the client)
- if ($old['recurrence']['EXDATE'])
+ if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE']))
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
- if ($old['recurrence']['EXCEPTIONS'])
+ if (isset($event['recurrence']['EXCEPTIONS']))
+ $with_exceptions = true; // exceptions already provided (e.g. from iCal import)
+ else if ($old['recurrence']['EXCEPTIONS'])
$event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
switch ($savemode) {
@@ -907,17 +914,22 @@ class kolab_driver extends calendar_driver
$event['recurrence'] = array();
$event['thisandfuture'] = $savemode == 'future';
+ // TODO: increment sequence if scheduling is affected
+
// remove some internal properties which should not be saved
- unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
+ unset($event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
// save properties to a recurrence exception instance
- if ($old['recurrence_id']) {
- $i = $old['_instance'] - 1;
- if (!empty($master['recurrence']['EXCEPTIONS'][$i])) {
- $master['recurrence']['EXCEPTIONS'][$i] = $event;
- $success = $storage->update_event($master, $old['id']);
- break;
+ 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;
}
+ }
}
$add_exception = true;
@@ -936,6 +948,7 @@ class kolab_driver extends calendar_driver
// save as new exception to master event
if ($add_exception) {
+ $event['_instance'] = $old['_instance'];
$master['recurrence']['EXCEPTIONS'][] = $event;
}
$success = $storage->update_event($master);
@@ -955,10 +968,11 @@ class kolab_driver extends calendar_driver
$new_duration = $event['end']->format('U') - $event['start']->format('U');
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
+ $date_shift = $old['start']->diff($event['start']);
// shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
- $event['start'] = $master['start']->add($old['start']->diff($event['start']));
+ $event['start'] = $master['start']->add($date_shift);
$event['end'] = clone $event['start'];
$event['end']->add(new DateInterval('PT'.$new_duration.'S'));
@@ -976,6 +990,20 @@ class kolab_driver extends calendar_driver
$event['end'] = $master['end'];
}
+ // 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']['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);
+ }
+ }
+ }
+
// unset _dateonly flags in (cached) date objects
unset($event['start']->_dateonly, $event['end']->_dateonly);
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 127ee36..4163cfb 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -318,6 +318,7 @@ class libvcalendar implements Iterator
if (!$seen[$object['uid']]++) {
// parse recurrence exceptions
if ($object['recurrence']) {
+ $object['recurrence']['EXCEPTIONS'] = array();
foreach ($vobject->children as $component) {
if ($component->name == 'VEVENT' && isset($component->{'RECURRENCE-ID'})) {
try {
@@ -455,6 +456,9 @@ class libvcalendar implements Iterator
case 'RECURRENCE-ID':
$event['recurrence_date'] = self::convert_datetime($prop);
+ if ($prop->offsetGet('RANGE') == 'THISANDFUTURE' || $prop->offsetGet('THISANDFUTURE') !== null) {
+ $event['thisandfuture'] = true;
+ }
break;
case 'RELATED-TO':
@@ -1151,11 +1155,10 @@ class libvcalendar implements Iterator
// append recurrence exceptions
if (is_array($event['recurrence']) && $event['recurrence']['EXCEPTIONS']) {
foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
- $exdate = clone $event['start'];
- $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
- $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $exdate, true);
- // if ($ex['thisandfuture']) // not supported by any client :-(
- // $recurrence_id->add('RANGE', 'THISANDFUTURE');
+ $exdate = $ex['recurrence_date'] ?: $ex['start'];
+ $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $exdate, false, (bool)$event['allday']);
+ if ($ex['thisandfuture'])
+ $recurrence_id->add('RANGE', 'THISANDFUTURE');
$this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id);
}
}
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index 9e2f03b..f8b0d85 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -164,6 +164,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
$events = $ical->import_from_file(__DIR__ . '/resources/recurrence-id.ics', 'UTF-8');
$this->assertEquals(1, count($events), "Fall back to Component::getComponents() when getBaseComponents() is empty");
$this->assertInstanceOf('DateTime', $events[0]['recurrence_date'], "Recurrence-ID as date");
+ $this->assertTrue($events[0]['thisandfuture'], "Range=THISANDFUTURE");
}
/**
diff --git a/plugins/libcalendaring/tests/resources/recurrence-id.ics b/plugins/libcalendaring/tests/resources/recurrence-id.ics
index 41485f9..8229da2 100644
--- a/plugins/libcalendaring/tests/resources/recurrence-id.ics
+++ b/plugins/libcalendaring/tests/resources/recurrence-id.ics
@@ -22,7 +22,7 @@ DTSTART;TZID="W. Europe":20140230T150000
DTEND;TZID="W. Europe":20140230T163000
TRANSP:OPAQUE
RDATE;TZID="W. Europe";VALUE=PERIOD:20140227T140000/20140227T153000
-RECURRENCE-ID:20140227T130000Z
+RECURRENCE-ID;RANGE=THISANDFUTURE:20140227T130000Z
SEQUENCE:0
UID:7e93e8e8eef16f28aa33b78cd73613ebff
DTSTAMP:20140120T105609Z
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 8cad89a..03b5dde 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -94,13 +94,26 @@ class kolab_format_event extends kolab_format_xcal
$this->obj->setStatus($status);
// save recurrence exceptions
- if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
+ if (is_array($object['recurrence']) && is_array($object['recurrence']['EXCEPTIONS'])) {
+ $recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
$vexceptions = new vectorevent;
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
- $exevent->obj->setRecurrenceID(self::get_datetime($exception['start'], null, true), (bool)$exception['thisandfuture']);
+
+ // get value for recurrence-id
+ if (!empty($exception['recurrence_date']) && is_a($exception['recurrence_date'], 'DateTime')) {
+ $recurrence_id = $exception['recurrence_date'];
+ $compacted['_instance'] = $recurrence_id->format($recurrence_id_format);
+ }
+ else if (!empty($exception['_instance']) && strlen($exception['_instance']) > 4) {
+ $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $object['start']->getTimezone());
+ $compacted['recurrence_date'] = $recurrence_id;
+ }
+ $exevent->obj->setRecurrenceID(self::get_datetime($recurrence_id ?: $exception['start'], null, $object['allday']), (bool)$exception['thisandfuture']);
+
$vexceptions->push($exevent->obj);
+
// write cleaned-up exception data back to memory/cache
$object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($compacted, $object);
}
@@ -172,15 +185,27 @@ class kolab_format_event extends kolab_format_xcal
// this is an exception object
if ($this->obj->recurrenceID()->isValid()) {
$object['thisandfuture'] = $this->obj->thisAndFuture();
+ $object['recurrence_date'] = self::php_datetime($this->obj->recurrenceID());
}
// read exception event objects
else if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
$recurrence_exceptions = array();
+ $recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
for ($i=0; $i < $exceptions->size(); $i++) {
if (($exobj = $exceptions->get($i))) {
$exception = new kolab_format_event($exobj);
if ($exception->is_valid()) {
- $recurrence_exceptions[] = $this->expand_exception($exception->to_array(), $object);
+ $exdata = $exception->to_array();
+
+ // fix date-only recurrence ID saved by old versions
+ if ($exdata['recurrence_date'] && $exdata['recurrence_date']->_dateonly && !$object['allday']) {
+ $exdata['recurrence_date']->setTimezone($object['start']->getTimezone());
+ $exdata['recurrence_date']->setTime($object['start']->format('G'), intval($object['start']->format('i')), intval($object['start']->format('s')));
+ }
+
+ $recurrence_id = $exdata['recurrence_date'] ?: $exdata['start'];
+ $exdata['_instance'] = $recurrence_id->format($recurrence_id_format);
+ $recurrence_exceptions[] = $this->expand_exception($exdata, $object);
}
}
}
@@ -211,21 +236,21 @@ class kolab_format_event extends kolab_format_xcal
*/
private function compact_exception($exception, $master)
{
- $forbidden = array('recurrence','organizer','attendees','sequence');
+ $forbidden = array('recurrence','organizer','_attachments');
- foreach ($forbidden as $prop) {
- if (array_key_exists($prop, $exception)) {
- unset($exception[$prop]);
+ foreach ($forbidden as $prop) {
+ if (array_key_exists($prop, $exception)) {
+ unset($exception[$prop]);
+ }
}
- }
- foreach ($master as $prop => $value) {
- if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value) {
- unset($exception[$prop]);
+ foreach ($master as $prop => $value) {
+ if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value) {
+ unset($exception[$prop]);
+ }
}
- }
- return $exception;
+ return $exception;
}
/**
@@ -233,12 +258,16 @@ class kolab_format_event extends kolab_format_xcal
*/
private function expand_exception($exception, $master)
{
- foreach ($master as $prop => $value) {
- if (empty($exception[$prop]) && !empty($value))
- $exception[$prop] = $value;
- }
+ foreach ($master as $prop => $value) {
+ if (empty($exception[$prop]) && !empty($value)) {
+ $exception[$prop] = $value;
+ if ($prop == 'recurrence') {
+ unset($exception[$prop]['EXCEPTIONS']);
+ }
+ }
+ }
- return $exception;
+ return $exception;
}
}
More information about the commits
mailing list