From cf7de16e59a3c45b7a1bf843491be99d2027b61a Mon Sep 17 00:00:00 2001 From: Jamie McClymont Date: Thu, 3 Jan 2019 13:24:50 +1300 Subject: [PATCH] Handle default timezones in getVCalendarRange Also includes some PHPUnit-based tests for this function! --- inc/RRule.php | 20 +++--- testing/phpunit/RangeTest.php | 119 ++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 testing/phpunit/RangeTest.php diff --git a/inc/RRule.php b/inc/RRule.php index db215567..7819a343 100644 --- a/inc/RRule.php +++ b/inc/RRule.php @@ -1156,7 +1156,7 @@ function rdate_expand( $dtstart, $property, $component, $range_end = null, $is_d * * @return array An array keyed on the UTC dates, referring to the component */ -function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=null, $return_floating_times=false ) { +function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=null, $return_floating_times=false, $fallback_tzid=null ) { $expansion = array(); $recur = $component->GetProperty($property); @@ -1165,7 +1165,7 @@ function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=nul $this_start = $component->GetProperty('DTSTART'); if ( isset($this_start) ) { - $this_start = new RepeatRuleDateTime($this_start); + $this_start = RepeatRuleDateTime::withFallbackTzid($this_start, $fallback_tzid); } else { $this_start = clone($dtstart); @@ -1418,12 +1418,12 @@ function expand_event_instances( vComponent $vResource, $range_start = null, $ra * @throws Exception (1) When DTSTART is not present but the RFC says MUST and (2) when we get an unsupported component * @return RepeatRuleDateRange */ -function getComponentRange(vComponent $comp) { +function getComponentRange(vComponent $comp, string $fallback_tzid = null) { $dtstart_prop = $comp->GetProperty('DTSTART'); $duration_prop = $comp->GetProperty('DURATION'); if ( isset($duration_prop) ) { if ( !isset($dtstart_prop) ) throw new Exception('Invalid '.$comp->GetType().' containing DURATION without DTSTART', 0); - $dtstart = new RepeatRuleDateTime($dtstart_prop); + $dtstart = RepeatRuleDateTime::withFallbackTzid($dtstart_prop, $fallback_tzid); $dtend = clone($dtstart); $dtend->modify(new Rfc5545Duration($duration_prop->Value())); } @@ -1447,17 +1447,17 @@ function getComponentRange(vComponent $comp) { } if ( isset($dtstart_prop) ) - $dtstart = new RepeatRuleDateTime($dtstart_prop); + $dtstart = RepeatRuleDateTime::withFallbackTzid($dtstart_prop, $fallback_tzid); else $dtstart = null; if ( isset($dtend_prop) ) - $dtend = new RepeatRuleDateTime($dtend_prop); + $dtend = RepeatRuleDateTime::withFallbackTzid($dtend_prop, $fallback_tzid); else $dtend = null; if ( isset($completed_prop) ) { - $completed = new RepeatRuleDateTime($completed_prop); + $completed = RepeatRuleDateTime::withFallbackTzid($completed_prop, $fallback_tzid); if ( !isset($dtstart) || (isset($dtstart) && $completed < $dtstart) ) $dtstart = $completed; if ( !isset($dtend) || (isset($dtend) && $completed > $dtend) ) $dtend = $completed; } @@ -1473,7 +1473,7 @@ function getComponentRange(vComponent $comp) { * @param object $vResource A vComponent which is a VCALENDAR containing components needing expansion * @return RepeatRuleDateRange Representing the range of time covered by the event. */ -function getVCalendarRange( $vResource ) { +function getVCalendarRange( $vResource, string $fallback_tzid = null ) { $components = $vResource->GetComponents(); $dtstart = null; @@ -1483,7 +1483,7 @@ function getVCalendarRange( $vResource ) { $has_repeats = false; foreach( $components AS $k => $comp ) { if ( $comp->GetType() == 'VTIMEZONE' ) continue; - $range = getComponentRange($comp); + $range = getComponentRange($comp, $fallback_tzid); $dtstart = $range->from; if ( !isset($dtstart) ) continue; $duration = $range->getDuration(); @@ -1502,7 +1502,7 @@ function getVCalendarRange( $vResource ) { $range_end = new RepeatRuleDateTime(); $range_end->modify('+150 years'); } - $instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end); + $instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end, null, false, $fallback_tzid); $instances += rdate_expand($dtstart, 'RDATE', $comp, $range_end); foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end) AS $k => $v ) { unset($instances[$k]); diff --git a/testing/phpunit/RangeTest.php b/testing/phpunit/RangeTest.php new file mode 100644 index 00000000..ac6d589c --- /dev/null +++ b/testing/phpunit/RangeTest.php @@ -0,0 +1,119 @@ +from->UTC()); + self::assertEquals("20190102T072000Z", (string) $range->until->UTC()); + + // TZ is specified in the event, so this should be unaffected by passing in a fallback timezone: + $range = getVCalendarRange($cal, "Asia/Baku"); + self::assertEquals("20181226T050000Z", (string) $range->from->UTC()); + self::assertEquals("20190102T072000Z", (string) $range->until->UTC()); + } + + public function testGetVCalendarRangeTwoDayAllDay() { + $cal = new vCalendar("BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20181218T052603Z +LAST-MODIFIED:20181218T052734Z +DTSTAMP:20181218T052734Z +UID:7e76efc0-b1ed-4b68-b0ed-9e34889c30bd +SUMMARY:New Event +RRULE:FREQ=WEEKLY;COUNT=3;BYDAY=TU +DTSTART;VALUE=DATE:20181225 +DTEND;VALUE=DATE:20181227 +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR"); + + $range = getVCalendarRange($cal, "Asia/Baku"); + self::assertEquals("20181224T200000Z", (string) $range->from->UTC()); + self::assertEquals("20190109T200000Z", (string) $range->until->UTC()); + } + + public function testGetVCalendarRangeFloating() { + // When interpreted as being in Greece, this event crosses the daylight savings boundary! + // TODO deal with how that affects all-day events... + $cal = new vCalendar("BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20181218T052603Z +LAST-MODIFIED:20181218T052734Z +DTSTAMP:20181218T052734Z +UID:7e76efc0-b1ed-4b68-b0ed-9e34889c30bd +SUMMARY:New Event +RRULE:FREQ=MONTHLY;COUNT=4;INTERVAL=2;BYMONTHDAY=10 +DTSTART:20180411T140000 +DTEND:20180411T150000 +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR"); + + $range = getVCalendarRange($cal, "Europe/Athens"); + self::assertEquals("20180411T110000Z", (string) $range->from->UTC()); + self::assertEquals("20181210T130000Z", (string) $range->until->UTC()); + } + + public function testGetVCalendarRangeAllDayAcrossDST() { + // When interpreted as being in Greece, this event crosses the daylight savings boundary! + + $cal = new vCalendar("BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20181218T052603Z +LAST-MODIFIED:20181218T052734Z +DTSTAMP:20181218T052734Z +UID:7e76efc0-b1ed-4b68-b0ed-9e34889c30bd +SUMMARY:New Event +RRULE:FREQ=MONTHLY;COUNT=5;INTERVAL=2;BYMONTHDAY=10 +DTSTART;VALUE=DATE:20180410 +DTEND;VALUE=DATE:20180411 +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR"); + + $range = getVCalendarRange($cal, "Europe/Athens"); + self::assertEquals("20180409T210000Z", (string) $range->from->UTC()); + + // This is correctly at a different UTC time-of-day to the original, since DST has changed + self::assertEquals("20181210T220000Z", (string) $range->until->UTC()); + } +}