Fix handling of Recurrence-ID properties for recurrence exceptions to comply with RFC 5545 (#4385)
This commit is contained in:
parent
49280a6f62
commit
ad55fc706d
6 changed files with 140 additions and 69 deletions
|
@ -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;
|
||||
$events = array();
|
||||
$exdates = 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']);
|
||||
|
||||
// read recurrence exceptions first
|
||||
$events = array();
|
||||
$exdata = array();
|
||||
$futuredata = array();
|
||||
$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]) {
|
||||
$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];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
|
|
@ -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])) {
|
||||
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,16 +914,21 @@ 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])) {
|
||||
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;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,7 +236,7 @@ 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)) {
|
||||
|
@ -234,8 +259,12 @@ 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))
|
||||
if (empty($exception[$prop]) && !empty($value)) {
|
||||
$exception[$prop] = $value;
|
||||
if ($prop == 'recurrence') {
|
||||
unset($exception[$prop]['EXCEPTIONS']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $exception;
|
||||
|
|
Loading…
Add table
Reference in a new issue