diff options
author | Christoph Wurst <christoph@winzerhof-wurst.at> | 2023-01-17 14:04:06 +0100 |
---|---|---|
committer | MichaIng (Rebase PR Action) <micha@dietpi.com> | 2023-02-27 14:38:43 +0000 |
commit | 8567d05ff4488fb6e0c7aae493b9be8c48814689 (patch) | |
tree | 114d8fb085c2f6f95fd5ef15e90f5981f53a47ab | |
parent | 038924ab12b0cd2a51a0f293629bacd80f432205 (diff) | |
download | nextcloud-server-8567d05ff4488fb6e0c7aae493b9be8c48814689.tar.gz nextcloud-server-8567d05ff4488fb6e0c7aae493b9be8c48814689.zip |
fix(caldav): Fix reminder timezone drift for all-day events
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
-rw-r--r-- | apps/dav/lib/CalDAV/CalDavBackend.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Reminder/ReminderService.php | 53 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php | 198 |
3 files changed, 237 insertions, 16 deletions
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 8108260797c..709f625afb3 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -675,7 +675,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp }|null + * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null */ public function getCalendarById(int $calendarId): ?array { $fields = array_column($this->propertyMap, 0); diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php index 1da471a51f5..a2daa3cc98e 100644 --- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php +++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php @@ -32,6 +32,7 @@ declare(strict_types=1); namespace OCA\DAV\CalDAV\Reminder; use DateTimeImmutable; +use DateTimeZone; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\Connector\Sabre\Principal; use OCP\AppFramework\Utility\ITimeFactory; @@ -221,6 +222,7 @@ class ReminderService { if (!$vcalendar) { return; } + $calendarTimeZone = $this->getCalendarTimeZone((int) $objectData['calendarid']); $vevents = $this->getAllVEventsFromVCalendar($vcalendar); if (count($vevents) === 0) { @@ -249,7 +251,7 @@ class ReminderService { continue; } - $alarms = $this->getRemindersForVAlarm($valarm, $objectData, + $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $eventHash, $alarmHash, true, true); $this->writeRemindersToDatabase($alarms); } @@ -306,6 +308,16 @@ class ReminderService { try { $triggerTime = $valarm->getEffectiveTriggerTime(); + /** + * @psalm-suppress DocblockTypeContradiction + * https://github.com/vimeo/psalm/issues/9244 + */ + if ($triggerTime->getTimezone() === false || $triggerTime->getTimezone()->getName() === 'UTC') { + $triggerTime = new DateTimeImmutable( + $triggerTime->format('Y-m-d H:i:s'), + $calendarTimeZone + ); + } } catch (InvalidDataException $e) { continue; } @@ -324,7 +336,7 @@ class ReminderService { continue; } - $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false); + $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $masterHash, $alarmHash, $isRecurring, false); $this->writeRemindersToDatabase($alarms); $processedAlarms[] = $alarmHash; } @@ -363,6 +375,7 @@ class ReminderService { /** * @param VAlarm $valarm * @param array $objectData + * @param DateTimeZone $calendarTimeZone * @param string|null $eventHash * @param string|null $alarmHash * @param bool $isRecurring @@ -371,6 +384,7 @@ class ReminderService { */ private function getRemindersForVAlarm(VAlarm $valarm, array $objectData, + DateTimeZone $calendarTimeZone, string $eventHash = null, string $alarmHash = null, bool $isRecurring = false, @@ -386,6 +400,16 @@ class ReminderService { $isRelative = $this->isAlarmRelative($valarm); /** @var DateTimeImmutable $notificationDate */ $notificationDate = $valarm->getEffectiveTriggerTime(); + /** + * @psalm-suppress DocblockTypeContradiction + * https://github.com/vimeo/psalm/issues/9244 + */ + if ($notificationDate->getTimezone() === false || $notificationDate->getTimezone()->getName() === 'UTC') { + $notificationDate = new DateTimeImmutable( + $notificationDate->format('Y-m-d H:i:s'), + $calendarTimeZone + ); + } $clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone()); $clonedNotificationDate->setTimestamp($notificationDate->getTimestamp()); @@ -471,6 +495,7 @@ class ReminderService { $vevents = $this->getAllVEventsFromVCalendar($vevent->parent); $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); $now = $this->timeFactory->getDateTime(); + $calendarTimeZone = $this->getCalendarTimeZone((int) $reminder['calendar_id']); try { $iterator = new EventIterator($vevents, $reminder['uid']); @@ -517,7 +542,7 @@ class ReminderService { $alarms = $this->getRemindersForVAlarm($valarm, [ 'calendarid' => $reminder['calendar_id'], 'id' => $reminder['object_id'], - ], $reminder['event_hash'], $alarmHash, true, false); + ], $calendarTimeZone, $reminder['event_hash'], $alarmHash, true, false); $this->writeRemindersToDatabase($alarms); // Abort generating reminders after creating one successfully @@ -825,4 +850,26 @@ class ReminderService { private function isRecurring(VEvent $vevent):bool { return isset($vevent->RRULE) || isset($vevent->RDATE); } + + /** + * @param int $calendarid + * + * @return DateTimeZone + */ + private function getCalendarTimeZone(int $calendarid): DateTimeZone { + $calendarInfo = $this->caldavBackend->getCalendarById($calendarid); + $tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone'; + if (!isset($calendarInfo[$tzProp])) { + // Defaulting to UTC + return new DateTimeZone('UTC'); + } + // This property contains a VCALENDAR with a single VTIMEZONE + /** @var string $timezoneProp */ + $timezoneProp = $calendarInfo[$tzProp]; + /** @var VObject\Component\VCalendar $vtimezoneObj */ + $vtimezoneObj = VObject\Reader::read($timezoneProp); + /** @var VObject\Component\VTimeZone $vtimezone */ + $vtimezone = $vtimezoneObj->VTIMEZONE; + return $vtimezone->getTimeZone(); + } } diff --git a/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php index 79d9376650b..fa8dd826e51 100644 --- a/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php +++ b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php @@ -29,6 +29,8 @@ declare(strict_types=1); */ namespace OCA\DAV\Tests\unit\CalDAV\Reminder; +use DateTime; +use DateTimeZone; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Reminder\Backend; use OCA\DAV\CalDAV\Reminder\INotificationProvider; @@ -193,6 +195,87 @@ END:VEVENT END:VCALENDAR EOD; + private const CALENDAR_DATA_ONE_TIME = <<<EOD +BEGIN:VCALENDAR +PRODID:-//IDN nextcloud.com//Calendar app 4.3.0-alpha.0//EN +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20230203T154600Z +DTSTAMP:20230203T154602Z +LAST-MODIFIED:20230203T154602Z +SEQUENCE:2 +UID:f6a565b6-f9a8-4d1e-9d01-c8dcbe716b7e +DTSTART;TZID=Europe/Vienna:20230204T090000 +DTEND;TZID=Europe/Vienna:20230204T120000 +STATUS:CONFIRMED +SUMMARY:TEST +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:-PT1H +END:VALARM +END:VEVENT +BEGIN:VTIMEZONE +TZID:Europe/Vienna +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +END:VCALENDAR +EOD; + + private const CALENDAR_DATA_ALL_DAY = <<<EOD +BEGIN:VCALENDAR +PRODID:-//IDN nextcloud.com//Calendar app 4.3.0-alpha.0//EN +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20230203T113430Z +DTSTAMP:20230203T113432Z +LAST-MODIFIED:20230203T113432Z +SEQUENCE:2 +UID:a163a056-ba26-44a2-8080-955f19611a8f +DTSTART;VALUE=DATE:20230204 +DTEND;VALUE=DATE:20230205 +STATUS:CONFIRMED +SUMMARY:TEST +BEGIN:VALARM +ACTION:EMAIL +TRIGGER;RELATED=START:-PT1H +END:VALARM +END:VEVENT +END:VCALENDAR +EOD; + + private const PAGO_PAGO_VTIMEZONE_ICS = <<<ICS +BEGIN:VCALENDAR +BEGIN:VTIMEZONE +TZID:Pacific/Pago_Pago +BEGIN:STANDARD +TZOFFSETFROM:-1100 +TZOFFSETTO:-1100 +TZNAME:SST +DTSTART:19700101T000000 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR +ICS; + + + /** @var null|string */ + private $oldTimezone; + protected function setUp(): void { parent::setUp(); @@ -253,7 +336,7 @@ EOD; $this->timeFactory->expects($this->once()) ->method('getDateTime') ->with() - ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00')); $this->reminderService->onCalendarObjectCreate($objectData); } @@ -280,7 +363,7 @@ EOD; $this->timeFactory->expects($this->once()) ->method('getDateTime') ->with() - ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00')); $this->reminderService->onCalendarObjectCreate($objectData); } @@ -303,8 +386,7 @@ EOD; $this->timeFactory->expects($this->once()) ->method('getDateTime') - ->with() - ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-29T00:00:00+00:00')); + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-29T00:00:00+00:00')); $this->reminderService->onCalendarObjectCreate($objectData); } @@ -323,6 +405,60 @@ EOD; $this->reminderService->onCalendarObjectCreate($objectData); } + public function testOnCalendarObjectCreateAllDayWithoutTimezone(): void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_ALL_DAY, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00')); + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null, + ]); + + // One hour before midnight relative to the server's time + $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00'))->getTimestamp(); + $this->backend->expects(self::once()) + ->method('insertReminder') + ->with(1337, 42, self::anything(), false, 1675468800, false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateAllDayWithTimezone(): void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_ALL_DAY, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00')); + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => self::PAGO_PAGO_VTIMEZONE_ICS, + ]); + + // One hour before midnight relative to the timezone + $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00', new DateTimeZone('Pacific/Pago_Pago')))->getTimestamp(); + $this->backend->expects(self::once()) + ->method('insertReminder') + ->with(1337, 42, 'a163a056-ba26-44a2-8080-955f19611a8f', false, self::anything(), false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + public function testOnCalendarObjectCreateRecurringEntryWithRepeat():void { $objectData = [ 'calendardata' => self::CALENDAR_DATA_RECURRING_REPEAT, @@ -330,7 +466,12 @@ EOD; 'calendarid' => '1337', 'component' => 'vevent', ]; - + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null, + ]); $this->backend->expects($this->exactly(6)) ->method('insertReminder') ->withConsecutive( @@ -342,11 +483,44 @@ EOD; [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false] ) ->willReturn(1); + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-29T00:00:00+00:00')); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + public function testOnCalendarObjectCreateWithEventTimezoneAndCalendarTimezone():void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_ONE_TIME, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => self::PAGO_PAGO_VTIMEZONE_ICS, + ]); + $expectedReminderTimstamp = (new DateTime('2023-02-04T08:00:00', new DateTimeZone('Europe/Vienna')))->getTimestamp(); + $this->backend->expects(self::once()) + ->method('insertReminder') + ->withConsecutive( + [1337, 42, self::anything(), false, self::anything(), false, self::anything(), self::anything(), self::anything(), true, $expectedReminderTimstamp, false], + ) + ->willReturn(1); + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null, + ]); $this->timeFactory->expects($this->once()) ->method('getDateTime') ->with() - ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-29T00:00:00+00:00')); + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));; $this->reminderService->onCalendarObjectCreate($objectData); } @@ -512,7 +686,7 @@ EOD; $provider1->expects($this->once()) ->method('send') ->with($this->callback(function ($vevent) { - if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { return false; } return true; @@ -520,7 +694,7 @@ EOD; $provider2->expects($this->once()) ->method('send') ->with($this->callback(function ($vevent) { - if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { return false; } return true; @@ -528,7 +702,7 @@ EOD; $provider3->expects($this->once()) ->method('send') ->with($this->callback(function ($vevent) { - if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { return false; } return true; @@ -536,7 +710,7 @@ EOD; $provider4->expects($this->once()) ->method('send') ->with($this->callback(function ($vevent) { - if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-30T00:00:00+00:00') { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-30T00:00:00+00:00') { return false; } return true; @@ -544,7 +718,7 @@ EOD; $provider5->expects($this->once()) ->method('send') ->with($this->callback(function ($vevent) { - if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-07-07T00:00:00+00:00') { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-07-07T00:00:00+00:00') { return false; } return true; @@ -592,7 +766,7 @@ EOD; ->willReturn(99); $this->timeFactory->method('getDateTime') - ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00')); $this->reminderService->processReminders(); } |