diff --git a/src/Generators/BaseOutlook.php b/src/Generators/BaseOutlook.php index 3fd19e5..bcfc503 100644 --- a/src/Generators/BaseOutlook.php +++ b/src/Generators/BaseOutlook.php @@ -35,17 +35,15 @@ public function generate(Link $link): string { $url = $this->baseUrl(); - $dateTimeFormat = $link->allDay ? $this->dateFormat : $this->dateTimeFormat; - - $utcStartDateTime = (clone $link->from)->setTimezone(new DateTimeZone('UTC')); - $utcEndDateTime = (clone $link->to)->setTimezone(new DateTimeZone('UTC')); - - $url .= '&startdt='.$utcStartDateTime->format($dateTimeFormat); - $url .= '&enddt='.$utcEndDateTime->format($dateTimeFormat); - if ($link->allDay) { + $url .= '&startdt='.$link->from->format($this->dateFormat); + $url .= '&enddt='.$link->to->format($this->dateFormat); $url .= '&allday=true'; } + else { + $url .= '&startdt='.(clone $link->from)->setTimezone(new DateTimeZone('UTC'))->format($this->dateTimeFormat); + $url .= '&enddt='.(clone $link->to)->setTimezone(new DateTimeZone('UTC'))->format($this->dateTimeFormat); + } $url .= '&subject='.$this->sanitizeString($link->title); diff --git a/src/Generators/Google.php b/src/Generators/Google.php index 728c6f9..18a3b07 100644 --- a/src/Generators/Google.php +++ b/src/Generators/Google.php @@ -15,7 +15,7 @@ class Google implements Generator /** @var string {@see https://www.php.net/manual/en/function.date.php} */ protected $dateFormat = 'Ymd'; /** @var string */ - protected $dateTimeFormat = 'Ymd\THis\Z'; + protected $dateTimeFormat = 'Ymd\THis'; /** @psalm-var GoogleUrlParameters */ protected array $urlParameters = []; @@ -31,19 +31,9 @@ public function generate(Link $link): string { $url = 'https://calendar.google.com/calendar/render?action=TEMPLATE'; - $utcStartDateTime = (clone $link->from)->setTimezone(new DateTimeZone('UTC')); - $utcEndDateTime = (clone $link->to)->setTimezone(new DateTimeZone('UTC')); $dateTimeFormat = $link->allDay ? $this->dateFormat : $this->dateTimeFormat; - $url .= '&dates='.$utcStartDateTime->format($dateTimeFormat).'/'.$utcEndDateTime->format($dateTimeFormat); - - // Add timezone name if it is specified in both from and to dates and is the same for both - if ( - $link->from->getTimezone() && $link->to->getTimezone() - && $link->from->getTimezone()->getName() === $link->to->getTimezone()->getName() - ) { - $url .= '&ctz=' . $link->from->getTimezone()->getName(); - } - + $url .= '&dates='.$link->from->format($dateTimeFormat).'/'.$link->to->format($dateTimeFormat); + $url .= '&ctz=' . $link->from->getTimezone()->getName(); $url .= '&text='.urlencode($link->title); if ($link->description) { diff --git a/src/Link.php b/src/Link.php index 54c9ab3..acf665a 100644 --- a/src/Link.php +++ b/src/Link.php @@ -43,15 +43,20 @@ class Link public function __construct(string $title, \DateTimeInterface $from, \DateTimeInterface $to, bool $allDay = false) { + $this->from = clone $from; + $this->to = clone $to; $this->title = $title; $this->allDay = $allDay; - if ($from > $to) { - throw InvalidLink::negativeDateRange($from, $to); + // Ensures timezones match. + if ($this->from->getTimezone()->getName() !== $this->to->getTimezone()->getName()) { + $this->to->setTimezone($from->getTimezone()); } - $this->from = clone $from; - $this->to = clone $to; + // Ensures from date is earlier than to date. + if ($this->from > $this->to) { + throw InvalidLink::negativeDateRange($from, $to); + } } /** @@ -65,15 +70,15 @@ public function __construct(string $title, \DateTimeInterface $from, \DateTimeIn */ public static function create(string $title, \DateTimeInterface $from, \DateTimeInterface $to, bool $allDay = false) { - // When creating all day events, we need to be in the UTC timezone as all day events are "floating" based on the user's timezone - if ($allDay) { - $startDate = new \DateTime($from->format('Y-m-d'), new \DateTimeZone('UTC')); - $numberOfDays = $from->diff($to)->days + 1; + $from_date = clone $from; + $to_date = clone $to; - return self::createAllDay($title, $startDate, $numberOfDays); + // If all day, we need to add 1 day to end date to get the correct duration. + if ($allDay) { + $to_date->modify('+1 day'); } - return new static($title, $from, $to, $allDay); + return new static($title, $from_date, $to_date, $allDay); } /** @@ -86,12 +91,7 @@ public static function create(string $title, \DateTimeInterface $from, \DateTime */ public static function createAllDay(string $title, \DateTimeInterface $fromDate, int $numberOfDays = 1): self { - // In cases where the from date is not UTC, make sure it's UTC, size all day events are floating and non UTC dates cause bugs in the generators - if ($fromDate->getTimezone() !== new \DateTimeZone('UTC')) { - $fromDate = \DateTime::createFromFormat('Y-m-d', $fromDate->format('Y-m-d')); - } - - $from = (clone $fromDate)->modify('midnight'); + $from = (clone $fromDate); $to = (clone $from)->modify("+$numberOfDays days"); return new self($title, $from, $to, true); diff --git a/tests/Generators/GoogleGeneratorTest.php b/tests/Generators/GoogleGeneratorTest.php index f068640..de5964f 100644 --- a/tests/Generators/GoogleGeneratorTest.php +++ b/tests/Generators/GoogleGeneratorTest.php @@ -36,6 +36,15 @@ public function it_correctly_generates_all_day_events_by_dates(): void ); } + /** @test */ + + public function it_correctly_generates_all_day_events_by_dates_diff_tz(): void + { + $this->assertMatchesSnapshot( + $this->generator()->generate($this->createEventMultipleDaysViaStartEndWithDiffTimezoneLink()) + ); + } + /** @test */ public function it_can_generate_an_url_with_custom_parameters(): void { diff --git a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_description_is_html_code_event_link_with_allday_flag__1.txt b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_description_is_html_code_event_link_with_allday_flag__1.txt index 539a65c..7f8a806 100644 --- a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_description_is_html_code_event_link_with_allday_flag__1.txt +++ b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_description_is_html_code_event_link_with_allday_flag__1.txt @@ -1 +1 @@ -https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20180201T090000Z/20180201T180000Z&ctz=UTC&text=Birthday+Party+%2B1&details=With+balloons%2C+clowns+and+stuff%0ABring+a+dog%2C+bring+a+frog.%0AThere+will+be+line+breaks+on+it.%0AProject+link+%3Ca+href%3D%22https%3A%2F%2Fgithub.com%2Fspatie%2Fcalendar-links%22%3Ecalendar-links%3C%2Fa%3E%0A%3Cimg+src%3D%22https%3A%2F%2Fgithub-ads.s3.eu-central-1.amazonaws.com%2Fcalendar-links.jpg%3Ft%3D1%22+width%3D%22419px%22+%2F%3E%0A%3Cbr%3E%0AThank+you.%0A&location=Party+Lane+1A%2C+1337+Funtown \ No newline at end of file +https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20180201T090000/20180201T180000&ctz=UTC&text=Birthday+Party+%2B1&details=With+balloons%2C+clowns+and+stuff%0ABring+a+dog%2C+bring+a+frog.%0AThere+will+be+line+breaks+on+it.%0AProject+link+%3Ca+href%3D%22https%3A%2F%2Fgithub.com%2Fspatie%2Fcalendar-links%22%3Ecalendar-links%3C%2Fa%3E%0A%3Cimg+src%3D%22https%3A%2F%2Fgithub-ads.s3.eu-central-1.amazonaws.com%2Fcalendar-links.jpg%3Ft%3D1%22+width%3D%22419px%22+%2F%3E%0A%3Cbr%3E%0AThank+you.%0A&location=Party+Lane+1A%2C+1337+Funtown \ No newline at end of file diff --git a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_multiple_days_event_link__1.txt b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_multiple_days_event_link__1.txt index c26c304..a261d46 100644 --- a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_multiple_days_event_link__1.txt +++ b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_multiple_days_event_link__1.txt @@ -1 +1 @@ -https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20191231T230000Z/20200101T010000Z&ctz=UTC&text=New+Year&details=With+balloons%2C+clowns+and+stuff%0ABring+a+dog%2C+bring+a+frog&location=Party+Lane+1A%2C+1337+Funtown \ No newline at end of file +https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20191231T230000/20200101T010000&ctz=UTC&text=New+Year&details=With+balloons%2C+clowns+and+stuff%0ABring+a+dog%2C+bring+a+frog&location=Party+Lane+1A%2C+1337+Funtown \ No newline at end of file diff --git a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_short_event_link__1.txt b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_short_event_link__1.txt index 160071b..3274d9c 100644 --- a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_short_event_link__1.txt +++ b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_a_short_event_link__1.txt @@ -1 +1 @@ -https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20180201T090000Z/20180201T180000Z&ctz=UTC&text=Birthday&details=With+balloons%2C+clowns+and+stuff%0ABring+a+dog%2C+bring+a+frog&location=Party+Lane+1A%2C+1337+Funtown \ No newline at end of file +https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20180201T090000/20180201T180000&ctz=UTC&text=Birthday&details=With+balloons%2C+clowns+and+stuff%0ABring+a+dog%2C+bring+a+frog&location=Party+Lane+1A%2C+1337+Funtown \ No newline at end of file diff --git a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_an_url_with_custom_parameters__1.txt b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_an_url_with_custom_parameters__1.txt index 1f254ff..8405569 100644 --- a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_an_url_with_custom_parameters__1.txt +++ b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_can_generate_an_url_with_custom_parameters__1.txt @@ -1 +1 @@ -https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20180201T090000Z/20180201T180000Z&ctz=UTC&text=Birthday&details=With+balloons%2C+clowns+and+stuff%0ABring+a+dog%2C+bring+a+frog&location=Party+Lane+1A%2C+1337+Funtown&recur=RRULE%3AFREQ%3DDAILY \ No newline at end of file +https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20180201T090000/20180201T180000&ctz=UTC&text=Birthday&details=With+balloons%2C+clowns+and+stuff%0ABring+a+dog%2C+bring+a+frog&location=Party+Lane+1A%2C+1337+Funtown&recur=RRULE%3AFREQ%3DDAILY \ No newline at end of file diff --git a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_dates__1.txt b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_dates__1.txt index 2feec82..e075b15 100644 --- a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_dates__1.txt +++ b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_dates__1.txt @@ -1 +1 @@ -https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20240125/20240131&ctz=UTC&text=All+day+bugs&details=Testing+all+day \ No newline at end of file +https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20240125/20240131&ctz=Pacific/Wake&text=All+day+bugs&details=Testing+all+day \ No newline at end of file diff --git a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_dates_diff_tz__1.txt b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_dates_diff_tz__1.txt new file mode 100644 index 0000000..fb0769e --- /dev/null +++ b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_dates_diff_tz__1.txt @@ -0,0 +1 @@ +https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20240125/20240201&ctz=Pacific/Wake&text=All+day+bugs&details=Testing+all+day \ No newline at end of file diff --git a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_days__1.txt b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_days__1.txt index 3f414ee..8239b39 100644 --- a/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_days__1.txt +++ b/tests/Generators/__snapshots__/GoogleGeneratorTest__it_correctly_generates_all_day_events_by_days__1.txt @@ -1 +1 @@ -https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20240125/20240130&ctz=UTC&text=All+day+bugs&details=Testing+all+day \ No newline at end of file +https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20240125/20240130&ctz=Pacific/Wake&text=All+day+bugs&details=Testing+all+day \ No newline at end of file diff --git a/tests/Generators/__snapshots__/IcsGeneratorTest__it_correctly_generates_all_day_events_by_dates__1.txt b/tests/Generators/__snapshots__/IcsGeneratorTest__it_correctly_generates_all_day_events_by_dates__1.txt index 28af5d4..ad20f49 100644 --- a/tests/Generators/__snapshots__/IcsGeneratorTest__it_correctly_generates_all_day_events_by_dates__1.txt +++ b/tests/Generators/__snapshots__/IcsGeneratorTest__it_correctly_generates_all_day_events_by_dates__1.txt @@ -2,7 +2,7 @@ BEGIN:VCALENDAR VERSION:2.0 PRODID:Spatie calendar-links BEGIN:VEVENT -UID:8fe3eececcd6b020db3b47ef55e7cf89 +UID:cb8469bae9cf10a9ee8f1179c6a932f5 SUMMARY:All day bugs DTSTAMP:20240125 DTSTART:20240125 diff --git a/tests/Generators/__snapshots__/IcsGeneratorTest__it_correctly_generates_all_day_events_by_days__1.txt b/tests/Generators/__snapshots__/IcsGeneratorTest__it_correctly_generates_all_day_events_by_days__1.txt index f159c15..d230a35 100644 --- a/tests/Generators/__snapshots__/IcsGeneratorTest__it_correctly_generates_all_day_events_by_days__1.txt +++ b/tests/Generators/__snapshots__/IcsGeneratorTest__it_correctly_generates_all_day_events_by_days__1.txt @@ -2,7 +2,7 @@ BEGIN:VCALENDAR VERSION:2.0 PRODID:Spatie calendar-links BEGIN:VEVENT -UID:a05fc4dac68ae6064aaae69dcdfd60a6 +UID:b4be522f87b9894dadd2b9cd5479136b SUMMARY:All day bugs DTSTAMP:20240125 DTSTART:20240125 diff --git a/tests/TestCase.php b/tests/TestCase.php index 903f6ae..28a6de9 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -50,7 +50,7 @@ protected function createSingleDayAllDayEventLink(bool $immutable = false): Link return Link::createAllDay( 'Birthday', - $dateTimeClass::createFromFormat('Y-m-d', '2018-02-01', new DateTimeZone('UTC')) + $dateTimeClass::createFromFormat('Y-m-d', '2018-02-01', new DateTimeZone('UTC'))->setTime(0, 0) )->description($description)->address('Party Lane 1A, 1337 Funtown'); } @@ -63,7 +63,7 @@ protected function createMultipleDaysAllDayEventLink(bool $immutable = false): L return Link::createAllDay( 'Birthday', - $dateTimeClass::createFromFormat('Y-m-d', '2018-02-01', new DateTimeZone('UTC')), + $dateTimeClass::createFromFormat('Y-m-d', '2018-02-01', new DateTimeZone('UTC'))->setTime(0, 0), 5 )->description($description)->address('Party Lane 1A, 1337 Funtown'); } @@ -95,6 +95,21 @@ protected function createEventMultipleDaysViaStartEndWithTimezoneLink(bool $immu )->description($description); } + protected function createEventMultipleDaysViaStartEndWithDiffTimezoneLink(bool $immutable = false): Link + { + $description = 'Testing all day'; + + $dateTimeClass = $immutable ? DateTimeImmutable::class : DateTime::class; + + // This should result in 7 days duration (2024-01-25 00:00 to 2024-01-31 00:00 Pacific/Wake). + return Link::create( + 'All day bugs', + $dateTimeClass::createFromFormat('Y-m-d', '2024-01-25', new DateTimeZone('Pacific/Wake'))->setTime(0, 0), + $dateTimeClass::createFromFormat('Y-m-d', '2024-01-30', new DateTimeZone('Europe/Luxembourg'))->setTime(13, 00), + true, + )->description($description); + } + protected function createDescriptionIsHTMLcodeEventLink(bool $immutable = false): Link { $description = 'With balloons, clowns and stuff