mirror of
https://github.com/nextcloud/server.git
synced 2026-06-08 16:26:59 -04:00
fix(caldav): Fix reminder timezone drift for all-day events
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
parent
3a06d335f1
commit
ba6e13d037
2 changed files with 237 additions and 16 deletions
|
|
@ -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;
|
||||
|
|
@ -205,6 +206,7 @@ class ReminderService {
|
|||
if (!$vcalendar) {
|
||||
return;
|
||||
}
|
||||
$calendarTimeZone = $this->getCalendarTimeZone((int) $objectData['calendarid']);
|
||||
|
||||
$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
|
||||
if (count($vevents) === 0) {
|
||||
|
|
@ -233,7 +235,7 @@ class ReminderService {
|
|||
continue;
|
||||
}
|
||||
|
||||
$alarms = $this->getRemindersForVAlarm($valarm, $objectData,
|
||||
$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone,
|
||||
$eventHash, $alarmHash, true, true);
|
||||
$this->writeRemindersToDatabase($alarms);
|
||||
}
|
||||
|
|
@ -286,6 +288,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;
|
||||
}
|
||||
|
|
@ -304,7 +316,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;
|
||||
}
|
||||
|
|
@ -343,6 +355,7 @@ class ReminderService {
|
|||
/**
|
||||
* @param VAlarm $valarm
|
||||
* @param array $objectData
|
||||
* @param DateTimeZone $calendarTimeZone
|
||||
* @param string|null $eventHash
|
||||
* @param string|null $alarmHash
|
||||
* @param bool $isRecurring
|
||||
|
|
@ -351,6 +364,7 @@ class ReminderService {
|
|||
*/
|
||||
private function getRemindersForVAlarm(VAlarm $valarm,
|
||||
array $objectData,
|
||||
DateTimeZone $calendarTimeZone,
|
||||
string $eventHash = null,
|
||||
string $alarmHash = null,
|
||||
bool $isRecurring = false,
|
||||
|
|
@ -366,6 +380,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());
|
||||
|
||||
|
|
@ -451,6 +475,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']);
|
||||
|
|
@ -496,7 +521,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
|
||||
|
|
@ -800,4 +825,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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -194,6 +196,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();
|
||||
|
||||
|
|
@ -250,7 +333,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);
|
||||
}
|
||||
|
|
@ -277,7 +360,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);
|
||||
}
|
||||
|
|
@ -300,8 +383,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);
|
||||
}
|
||||
|
|
@ -320,6 +402,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,
|
||||
|
|
@ -327,7 +463,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(
|
||||
|
|
@ -339,11 +480,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'));
|
||||
->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, '2023-02-03T13:28:00+00:00'));;
|
||||
|
||||
$this->reminderService->onCalendarObjectCreate($objectData);
|
||||
}
|
||||
|
|
@ -509,7 +683,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;
|
||||
|
|
@ -517,7 +691,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;
|
||||
|
|
@ -525,7 +699,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;
|
||||
|
|
@ -533,7 +707,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;
|
||||
|
|
@ -541,7 +715,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;
|
||||
|
|
@ -589,7 +763,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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue