Fix bugs in expansion of events with overridden instances

This commit is contained in:
Jamie McClymont 2019-01-28 15:06:13 +13:00
parent 6a3619aaad
commit ffa06343a3
2 changed files with 205 additions and 25 deletions

View File

@ -1227,6 +1227,26 @@ function expand_event_instances( vComponent $vResource, $range_start = null, $ra
$is_date = false;
$has_repeats = false;
$dtstart_type = 'DTSTART';
$components_prefix = [];
$components_base_events = [];
$components_override_events = [];
foreach ($components AS $k => $comp) {
if ( $comp->GetType() != 'VEVENT' && $comp->GetType() != 'VTODO' && $comp->GetType() != 'VJOURNAL' ) {
// Other types of component (such as VTIMEZONE) go first
$components_prefix[] = $comp;
} else if ($comp->GetProperty('RECURRENCE-ID') === null) {
// This is the base event, we need to handle it first
$components_base_events[] = $comp;
} else {
// This is an override of an event instance, handle it last
$components_override_events[] = $comp;
}
}
$components = array_merge($components_prefix, $components_base_events, $components_override_events);
foreach( $components AS $k => $comp ) {
if ( $comp->GetType() != 'VEVENT' && $comp->GetType() != 'VTODO' && $comp->GetType() != 'VJOURNAL' ) {
continue;
@ -1371,36 +1391,43 @@ function expand_event_instances( vComponent $vResource, $range_start = null, $ra
$p = $comp->GetProperty('RECURRENCE-ID');
if ( isset($p) && $p->Value() != '') {
$recurrence_id = $p->Value();
if ( !isset($new_components[$recurrence_id]) ) {
// The component we're replacing is outside the range. Unless the replacement
// is *in* the range we will move along to the next one.
$dtstart_prop = $comp->GetProperty($dtstart_type);
if ( !isset($dtstart_prop) ) continue; // No start: no expansion. Note that we consider 'DUE' to be a start if DTSTART is missing
$dtstart = new RepeatRuleDateTime( $dtstart_prop );
$is_date = $dtstart->isDate();
if ( $return_floating_times ) $dtstart->setAsFloat();
$dtstart = $dtstart->FloatOrUTC($return_floating_times);
if ( $dtstart > $end_utc ) continue; // Start after end of range, skip it
$end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND');
$duration = $comp->GetProperty('DURATION');
if ( !isset($duration) || $duration->Value() == '' ) {
$instance_end = $comp->GetProperty($end_type);
if ( isset($instance_end) ) {
$dtend = new RepeatRuleDateTime( $instance_end );
if ( $return_floating_times ) $dtend->setAsFloat();
$dtend = $dtend->FloatOrUTC($return_floating_times);
}
else {
$dtend = $dtstart + ($is_date ? $dtstart + 86400 : 0 );
}
$dtstart_prop = $comp->GetProperty('DTSTART');
if ( !isset($dtstart_prop) && $comp->GetType() != 'VTODO' ) {
$dtstart_prop = $comp->GetProperty('DUE');
}
if ( !isset($new_components[$recurrence_id]) && !isset($dtstart_prop) ) continue; // No start: no expansion. Note that we consider 'DUE' to be a start if DTSTART is missing
$dtstart_rrdt = new RepeatRuleDateTime( $dtstart_prop );
$is_date = $dtstart_rrdt->isDate();
if ( $return_floating_times ) $dtstart_rrdt->setAsFloat();
$dtstart = $dtstart_rrdt->FloatOrUTC($return_floating_times);
if ( !isset($new_components[$recurrence_id]) && $dtstart > $end_utc ) continue; // Start after end of range, skip it
$end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND');
$duration = $comp->GetProperty('DURATION');
if ( !isset($duration) || $duration->Value() == '' ) {
$instance_end = $comp->GetProperty($end_type);
if ( isset($instance_end) ) {
$dtend_rrdt = new RepeatRuleDateTime( $instance_end );
if ( $return_floating_times ) $dtend_rrdt->setAsFloat();
$dtend = $dtend_rrdt->FloatOrUTC($return_floating_times);
$comp->AddProperty('DURATION', Rfc5545Duration::fromTwoDates($dtstart_rrdt, $dtend_rrdt) );
}
else {
$duration = new Rfc5545Duration($duration->Value());
$dtend = $dtstart + $duration->asSeconds();
$dtend = $dtstart + ($is_date ? $dtstart + 86400 : 0 );
}
if ( $dtend < $start_utc ) continue; // End before start of range: skip that too.
}
else {
$duration = new Rfc5545Duration($duration->Value());
$dtend = $dtstart + $duration->asSeconds();
}
if ( !isset($new_components[$recurrence_id]) && $dtend < $start_utc ) continue; // End before start of range: skip that too.
if ( DEBUG_RRULE ) printf( "Replacing overridden instance at %s\n", $recurrence_id);
$new_components[$recurrence_id] = $comp;
}

View File

@ -0,0 +1,153 @@
<?php
set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/share/awl/inc' . PATH_SEPARATOR . 'inc');
require_once('RRule.php');
require_once('vCalendar.php');
use PHPUnit\Framework\TestCase;
// 1PM-2PM Monday-Thursday (only for one week), NZ time
$base_cal = new vCalendar("BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20190117T001216Z
LAST-MODIFIED:20190117T001233Z
DTSTAMP:20190117T001233Z
UID:dae6404d-1ce0-42d0-af3b-0d303034197b
SUMMARY:New Event
RRULE:FREQ=DAILY;UNTIL=20190124T000000Z
DTSTART;TZID=Pacific/Auckland:20190121T130000
DTEND;TZID=Pacific/Auckland:20190121T140000
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR");
$tuesday_renamed_cal = new vCalendar("
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20190117T001216Z
LAST-MODIFIED:20190117T001805Z
DTSTAMP:20190117T001805Z
UID:d0d2df67-df7c-4b07-b729-221af3681c09
SUMMARY:New Event
RRULE:FREQ=DAILY;UNTIL=20190124T000000Z
DTSTART;TZID=Pacific/Auckland:20190121T130000
DTEND;TZID=Pacific/Auckland:20190121T140000
TRANSP:OPAQUE
X-MOZ-GENERATION:1
END:VEVENT
BEGIN:VEVENT
CREATED:20190117T001741Z
LAST-MODIFIED:20190117T001805Z
DTSTAMP:20190117T001805Z
UID:d0d2df67-df7c-4b07-b729-221af3681c09
SUMMARY:Tuesday has been renamed
RECURRENCE-ID;TZID=Pacific/Auckland:20190122T130000
DTSTART;TZID=Pacific/Auckland:20190122T130000
DTEND;TZID=Pacific/Auckland:20190122T140000
TRANSP:OPAQUE
X-MOZ-GENERATION:1
SEQUENCE:1
END:VEVENT
END:VCALENDAR
");
$tuesday_renamed_cal_order_swapped = new vCalendar("
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20190117T001741Z
LAST-MODIFIED:20190117T001805Z
DTSTAMP:20190117T001805Z
UID:d0d2df67-df7c-4b07-b729-221af3681c09
SUMMARY:Tuesday has been renamed
RECURRENCE-ID;TZID=Pacific/Auckland:20190122T130000
DTSTART;TZID=Pacific/Auckland:20190122T130000
DTEND;TZID=Pacific/Auckland:20190122T140000
TRANSP:OPAQUE
X-MOZ-GENERATION:1
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
CREATED:20190117T001216Z
LAST-MODIFIED:20190117T001805Z
DTSTAMP:20190117T001805Z
UID:d0d2df67-df7c-4b07-b729-221af3681c09
SUMMARY:New Event
RRULE:FREQ=DAILY;UNTIL=20190124T000000Z
DTSTART;TZID=Pacific/Auckland:20190121T130000
DTEND;TZID=Pacific/Auckland:20190121T140000
TRANSP:OPAQUE
X-MOZ-GENERATION:1
END:VEVENT
END:VCALENDAR
");
/**
* A simplified model of get_freebusy, which works off of a passed-in vCalendar
* rather than making SQL queries
*/
function get_freebusyish(vCalendar $cal) {
$expansion = expand_event_instances($cal)->GetComponents(['VEVENT' => true]);
$result = array();
foreach ($expansion as $k => $instance) {
// The same logic used in freebusy-functions (apart from default timezone
// handling, which isn't really under test here)
$start_date = new RepeatRuleDateTime($instance->GetProperty('DTSTART'));
$duration = $instance->GetProperty('DURATION');
$duration = (!isset($duration) ? 'P1D' : $duration->Value());
$end_date = clone($start_date);
$end_date->modify($duration);
array_push($result, $start_date->UTC() .'/'. $end_date->UTC());
}
sort($result);
return $result;
}
final class ExpansionTest extends TestCase
{
const expected_freebusyish_for_base = [
'20190121T000000Z/20190121T010000Z',
'20190122T000000Z/20190122T010000Z',
'20190123T000000Z/20190123T010000Z',
'20190124T000000Z/20190124T010000Z',
];
public function testUnmodifiedCal() {
global $base_cal;
self::assertEquals(
self::expected_freebusyish_for_base,
get_freebusyish($base_cal)
);
}
public function testTueRenamed() {
global $tuesday_renamed_cal;
self::assertEquals(
self::expected_freebusyish_for_base,
get_freebusyish($tuesday_renamed_cal)
);
}
public function testTueRenamedSwapped() {
global $tuesday_renamed_cal_order_swapped;
self::assertEquals(
self::expected_freebusyish_for_base,
get_freebusyish($tuesday_renamed_cal_order_swapped)
);
}
}