From d593206a30310de781dc456f965c8111900b01a9 Mon Sep 17 00:00:00 2001 From: Richard Steinmetz Date: Mon, 13 Nov 2023 17:36:24 +0100 Subject: [PATCH] feat(dav): dispatch out-of-office started and ended events Signed-off-by: Richard Steinmetz --- .../composer/composer/autoload_classmap.php | 4 + .../dav/composer/composer/autoload_static.php | 4 + .../OutOfOfficeEventDispatcherJob.php | 92 +++++++++ apps/dav/lib/CalDAV/TimezoneService.php | 98 ++++++++++ .../AvailabilitySettingsController.php | 15 +- apps/dav/lib/Db/Absence.php | 11 +- apps/dav/lib/Db/AbsenceMapper.php | 25 +++ apps/dav/lib/Db/Property.php | 53 ++++++ apps/dav/lib/Db/PropertyMapper.php | 57 ++++++ apps/dav/lib/Service/AbsenceService.php | 75 +++++--- .../integration/Db/PropertyMapperTest.php | 55 ++++++ .../OutOfOfficeEventDispatcherJobTest.php | 175 ++++++++++++++++++ .../tests/unit/CalDAV/TimezoneServiceTest.php | 161 ++++++++++++++++ config/config.sample.php | 10 + lib/composer/composer/autoload_classmap.php | 2 + lib/composer/composer/autoload_static.php | 2 + lib/private/User/AvailabilityCoordinator.php | 7 +- .../User/Events/OutOfOfficeEndedEvent.php | 51 +++++ .../User/Events/OutOfOfficeStartedEvent.php | 51 +++++ .../lib/User/AvailabilityCoordinatorTest.php | 18 +- 20 files changed, 920 insertions(+), 46 deletions(-) create mode 100644 apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php create mode 100644 apps/dav/lib/CalDAV/TimezoneService.php create mode 100644 apps/dav/lib/Db/Property.php create mode 100644 apps/dav/lib/Db/PropertyMapper.php create mode 100644 apps/dav/tests/integration/Db/PropertyMapperTest.php create mode 100644 apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php create mode 100644 apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php create mode 100644 lib/public/User/Events/OutOfOfficeEndedEvent.php create mode 100644 lib/public/User/Events/OutOfOfficeStartedEvent.php diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 7891cad42ebef..ef28133ed06d7 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -18,6 +18,7 @@ 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', 'OCA\\DAV\\BackgroundJob\\EventReminderJob' => $baseDir . '/../lib/BackgroundJob/EventReminderJob.php', 'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php', + 'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => $baseDir . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php', 'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => $baseDir . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php', 'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => $baseDir . '/../lib/BackgroundJob/RefreshWebcalJob.php', 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => $baseDir . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', @@ -100,6 +101,7 @@ 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => $baseDir . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php', 'OCA\\DAV\\CalDAV\\Status\\Status' => $baseDir . '/../lib/CalDAV/Status/Status.php', 'OCA\\DAV\\CalDAV\\Status\\StatusService' => $baseDir . '/../lib/CalDAV/Status/StatusService.php', + 'OCA\\DAV\\CalDAV\\TimezoneService' => $baseDir . '/../lib/CalDAV/TimezoneService.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php', 'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => $baseDir . '/../lib/CalDAV/Trashbin/Plugin.php', @@ -210,6 +212,8 @@ 'OCA\\DAV\\Db\\AbsenceMapper' => $baseDir . '/../lib/Db/AbsenceMapper.php', 'OCA\\DAV\\Db\\Direct' => $baseDir . '/../lib/Db/Direct.php', 'OCA\\DAV\\Db\\DirectMapper' => $baseDir . '/../lib/Db/DirectMapper.php', + 'OCA\\DAV\\Db\\Property' => $baseDir . '/../lib/Db/Property.php', + 'OCA\\DAV\\Db\\PropertyMapper' => $baseDir . '/../lib/Db/PropertyMapper.php', 'OCA\\DAV\\Direct\\DirectFile' => $baseDir . '/../lib/Direct/DirectFile.php', 'OCA\\DAV\\Direct\\DirectHome' => $baseDir . '/../lib/Direct/DirectHome.php', 'OCA\\DAV\\Direct\\Server' => $baseDir . '/../lib/Direct/Server.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index fdfe7feb6d908..73e680ef2e897 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -33,6 +33,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', 'OCA\\DAV\\BackgroundJob\\EventReminderJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/EventReminderJob.php', 'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php', + 'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php', 'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php', 'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/RefreshWebcalJob.php', 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', @@ -115,6 +116,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php', 'OCA\\DAV\\CalDAV\\Status\\Status' => __DIR__ . '/..' . '/../lib/CalDAV/Status/Status.php', 'OCA\\DAV\\CalDAV\\Status\\StatusService' => __DIR__ . '/..' . '/../lib/CalDAV/Status/StatusService.php', + 'OCA\\DAV\\CalDAV\\TimezoneService' => __DIR__ . '/..' . '/../lib/CalDAV/TimezoneService.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php', 'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/Plugin.php', @@ -225,6 +227,8 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Db\\AbsenceMapper' => __DIR__ . '/..' . '/../lib/Db/AbsenceMapper.php', 'OCA\\DAV\\Db\\Direct' => __DIR__ . '/..' . '/../lib/Db/Direct.php', 'OCA\\DAV\\Db\\DirectMapper' => __DIR__ . '/..' . '/../lib/Db/DirectMapper.php', + 'OCA\\DAV\\Db\\Property' => __DIR__ . '/..' . '/../lib/Db/Property.php', + 'OCA\\DAV\\Db\\PropertyMapper' => __DIR__ . '/..' . '/../lib/Db/PropertyMapper.php', 'OCA\\DAV\\Direct\\DirectFile' => __DIR__ . '/..' . '/../lib/Direct/DirectFile.php', 'OCA\\DAV\\Direct\\DirectHome' => __DIR__ . '/..' . '/../lib/Direct/DirectHome.php', 'OCA\\DAV\\Direct\\Server' => __DIR__ . '/..' . '/../lib/Direct/Server.php', diff --git a/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php new file mode 100644 index 0000000000000..9b219cf30da0e --- /dev/null +++ b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php @@ -0,0 +1,92 @@ + + * + * @author Richard Steinmetz + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\BackgroundJob; + +use OCA\DAV\CalDAV\TimezoneService; +use OCA\DAV\Db\AbsenceMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUserManager; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; +use Psr\Log\LoggerInterface; + +class OutOfOfficeEventDispatcherJob extends QueuedJob { + public const EVENT_START = 'start'; + public const EVENT_END = 'end'; + + public function __construct( + ITimeFactory $time, + private AbsenceMapper $absenceMapper, + private LoggerInterface $logger, + private IEventDispatcher $eventDispatcher, + private IUserManager $userManager, + private TimezoneService $timezoneService, + ) { + parent::__construct($time); + } + + public function run($argument): void { + $id = $argument['id']; + $event = $argument['event']; + + try { + $absence = $this->absenceMapper->findById($id); + } catch (DoesNotExistException | \OCP\DB\Exception $e) { + $this->logger->error('Failed to dispatch out-of-office event: ' . $e->getMessage(), [ + 'exception' => $e, + 'argument' => $argument, + ]); + return; + } + + $userId = $absence->getUserId(); + $user = $this->userManager->get($userId); + if ($user === null) { + $this->logger->error("Failed to dispatch out-of-office event: User $userId does not exist", [ + 'argument' => $argument, + ]); + return; + } + + $data = $absence->toOutOufOfficeData( + $user, + $this->timezoneService->getUserTimezone($userId) ?? $this->timezoneService->getDefaultTimezone(), + ); + if ($event === self::EVENT_START) { + $this->eventDispatcher->dispatchTyped(new OutOfOfficeStartedEvent($data)); + } elseif ($event === self::EVENT_END) { + $this->eventDispatcher->dispatchTyped(new OutOfOfficeEndedEvent($data)); + } else { + $this->logger->error("Invalid out-of-office event: $event", [ + 'argument' => $argument, + ]); + } + } +} diff --git a/apps/dav/lib/CalDAV/TimezoneService.php b/apps/dav/lib/CalDAV/TimezoneService.php new file mode 100644 index 0000000000000..bdbd0b9fe2cb5 --- /dev/null +++ b/apps/dav/lib/CalDAV/TimezoneService.php @@ -0,0 +1,98 @@ + + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\CalDAV; + +use OCA\DAV\Db\PropertyMapper; +use OCP\Calendar\ICalendar; +use OCP\Calendar\IManager; +use OCP\IConfig; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VTimeZone; +use Sabre\VObject\Reader; +use function array_reduce; + +class TimezoneService { + + public function __construct(private IConfig $config, + private PropertyMapper $propertyMapper, + private IManager $calendarManager) { + } + + public function getUserTimezone(string $userId): ?string { + $availabilityPropPath = 'calendars/' . $userId . '/inbox'; + $availabilityProp = '{' . Plugin::NS_CALDAV . '}calendar-availability'; + $availabilities = $this->propertyMapper->findPropertyByPathAndName($userId, $availabilityPropPath, $availabilityProp); + if (!empty($availabilities)) { + $availability = $availabilities[0]->getPropertyvalue(); + /** @var VCalendar $vCalendar */ + $vCalendar = Reader::read($availability); + /** @var VTimeZone $vTimezone */ + $vTimezone = $vCalendar->VTIMEZONE; + // Sabre has a fallback to date_default_timezone_get + return $vTimezone->getTimeZone()->getName(); + } + + $principal = 'principals/users/' . $userId; + $uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI); + $calendars = $this->calendarManager->getCalendarsForPrincipal($principal); + + /** @var ?VTimeZone $personalCalendarTimezone */ + $personalCalendarTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) use ($uri) { + if ($acc !== null) { + return $acc; + } + if ($calendar->getUri() === $uri && !$calendar->isDeleted() && $calendar instanceof CalendarImpl) { + return $calendar->getSchedulingTimezone(); + } + return null; + }); + if ($personalCalendarTimezone !== null) { + return $personalCalendarTimezone->getTimeZone()->getName(); + } + + // No timezone in the personalCalendarTimezone calendar or no personalCalendarTimezone calendar + // Loop through all calendars until we find a timezone. + /** @var ?VTimeZone $firstTimezone */ + $firstTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) { + if ($acc !== null) { + return $acc; + } + if (!$calendar->isDeleted() && $calendar instanceof CalendarImpl) { + return $calendar->getSchedulingTimezone(); + } + return null; + }); + if ($firstTimezone !== null) { + return $firstTimezone->getTimeZone()->getName(); + } + return null; + } + + public function getDefaultTimezone(): string { + return $this->config->getSystemValueString('default_timezone', 'UTC'); + } + +} diff --git a/apps/dav/lib/Controller/AvailabilitySettingsController.php b/apps/dav/lib/Controller/AvailabilitySettingsController.php index 3ff89fe87eb66..3e10162dd842d 100644 --- a/apps/dav/lib/Controller/AvailabilitySettingsController.php +++ b/apps/dav/lib/Controller/AvailabilitySettingsController.php @@ -35,11 +35,12 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\IRequest; +use OCP\IUserSession; class AvailabilitySettingsController extends Controller { public function __construct( IRequest $request, - private ?string $userId, + private ?IUserSession $userSession, private AbsenceService $absenceService, ) { parent::__construct(Application::APP_ID, $request); @@ -56,8 +57,8 @@ public function updateAbsence( string $status, string $message, ): Response { - $userId = $this->userId; - if ($userId === null) { + $user = $this->userSession?->getUser(); + if ($user === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -68,7 +69,7 @@ public function updateAbsence( } $absence = $this->absenceService->createOrUpdateAbsence( - $userId, + $user, $firstDay, $lastDay, $status, @@ -82,12 +83,12 @@ public function updateAbsence( */ #[NoAdminRequired] public function clearAbsence(): Response { - $userId = $this->userId; - if ($userId === null) { + $user = $this->userSession?->getUser(); + if ($user === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } - $this->absenceService->clearAbsence($userId); + $this->absenceService->clearAbsence($user); return new JSONResponse([]); } diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php index 8de8ecc9aa07e..3cd8037d57e4e 100644 --- a/apps/dav/lib/Db/Absence.php +++ b/apps/dav/lib/Db/Absence.php @@ -26,7 +26,8 @@ namespace OCA\DAV\Db; -use DateTimeImmutable; +use DateTime; +use DateTimeZone; use Exception; use InvalidArgumentException; use JsonSerializable; @@ -67,7 +68,7 @@ public function __construct() { $this->addType('message', 'string'); } - public function toOutOufOfficeData(IUser $user): IOutOfOfficeData { + public function toOutOufOfficeData(IUser $user, string $timezone): IOutOfOfficeData { if ($user->getUID() !== $this->getUserId()) { throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ", got " . $user->getUID()); } @@ -75,8 +76,10 @@ public function toOutOufOfficeData(IUser $user): IOutOfOfficeData { throw new Exception('Creating out-of-office data without ID'); } - $startDate = new DateTimeImmutable($this->getFirstDay()); - $endDate = new DateTimeImmutable($this->getLastDay()); + $tz = new DateTimeZone($timezone); + $startDate = new DateTime($this->getFirstDay(), $tz); + $endDate = new DateTime($this->getLastDay(), $tz); + $endDate->setTime(23, 59); return new OutOfOfficeData( (string)$this->getId(), $user, diff --git a/apps/dav/lib/Db/AbsenceMapper.php b/apps/dav/lib/Db/AbsenceMapper.php index 6e1133f779c33..7529d04cf1040 100644 --- a/apps/dav/lib/Db/AbsenceMapper.php +++ b/apps/dav/lib/Db/AbsenceMapper.php @@ -40,6 +40,31 @@ public function __construct(IDBConnection $db) { parent::__construct($db, 'dav_absence', Absence::class); } + /** + * @throws DoesNotExistException + * @throws \OCP\DB\Exception + */ + public function findById(int $id): Absence { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq( + 'id', + $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), + IQueryBuilder::PARAM_INT), + ); + try { + return $this->findEntity($qb); + } catch (MultipleObjectsReturnedException $e) { + // Won't happen as id is the primary key + throw new \RuntimeException( + 'The impossible has happened! The query returned multiple absence settings for one user.', + 0, + $e, + ); + } + } + /** * @throws DoesNotExistException * @throws \OCP\DB\Exception diff --git a/apps/dav/lib/Db/Property.php b/apps/dav/lib/Db/Property.php new file mode 100644 index 0000000000000..5234ad852b1a7 --- /dev/null +++ b/apps/dav/lib/Db/Property.php @@ -0,0 +1,53 @@ + + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Db; + +use OCP\AppFramework\Db\Entity; + +/** + * @method string getUserid() + * @method string getPropertypath() + * @method string getPropertyname() + * @method string getPropertyvalue() + */ +class Property extends Entity { + + /** @var string|null */ + protected $userid; + + /** @var string|null */ + protected $propertypath; + + /** @var string|null */ + protected $propertyname; + + /** @var string|null */ + protected $propertyvalue; + + /** @var int|null */ + protected $valuetype; + +} diff --git a/apps/dav/lib/Db/PropertyMapper.php b/apps/dav/lib/Db/PropertyMapper.php new file mode 100644 index 0000000000000..6f39d03d1c147 --- /dev/null +++ b/apps/dav/lib/Db/PropertyMapper.php @@ -0,0 +1,57 @@ + + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Db; + +use OCP\AppFramework\Db\QBMapper; +use OCP\IDBConnection; + +/** + * @template-extends QBMapper + */ +class PropertyMapper extends QBMapper { + + private const TABLE_NAME = 'properties'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, self::TABLE_NAME, Property::class); + } + + /** + * @return Property[] + */ + public function findPropertyByPathAndName(string $userId, string $path, string $name): array { + $selectQb = $this->db->getQueryBuilder(); + $selectQb->select('*') + ->from(self::TABLE_NAME) + ->where( + $selectQb->expr()->eq('userid', $selectQb->createNamedParameter($userId)), + $selectQb->expr()->eq('propertypath', $selectQb->createNamedParameter($path)), + $selectQb->expr()->eq('propertyname', $selectQb->createNamedParameter($name)), + ); + return $this->findEntities($selectQb); + } + +} diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php index b50dd32e925fd..3f5168e386d8e 100644 --- a/apps/dav/lib/Service/AbsenceService.php +++ b/apps/dav/lib/Service/AbsenceService.php @@ -27,11 +27,14 @@ namespace OCA\DAV\Service; use InvalidArgumentException; +use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob; +use OCA\DAV\CalDAV\TimezoneService; use OCA\DAV\Db\Absence; use OCA\DAV\Db\AbsenceMapper; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\IEventDispatcher; -use OCP\IUserManager; +use OCP\IUser; use OCP\User\Events\OutOfOfficeChangedEvent; use OCP\User\Events\OutOfOfficeClearedEvent; use OCP\User\Events\OutOfOfficeScheduledEvent; @@ -40,7 +43,8 @@ class AbsenceService { public function __construct( private AbsenceMapper $absenceMapper, private IEventDispatcher $eventDispatcher, - private IUserManager $userManager, + private IJobList $jobList, + private TimezoneService $timezoneService, ) { } @@ -52,61 +56,76 @@ public function __construct( * @throws InvalidArgumentException If no user with the given user id exists. */ public function createOrUpdateAbsence( - string $userId, + IUser $user, string $firstDay, string $lastDay, string $status, string $message, ): Absence { try { - $absence = $this->absenceMapper->findByUserId($userId); + $absence = $this->absenceMapper->findByUserId($user->getUID()); } catch (DoesNotExistException) { $absence = new Absence(); } - $absence->setUserId($userId); + $absence->setUserId($user->getUID()); $absence->setFirstDay($firstDay); $absence->setLastDay($lastDay); $absence->setStatus($status); $absence->setMessage($message); - // TODO: this method should probably just take a IUser instance - $user = $this->userManager->get($userId); - if ($user === null) { - throw new InvalidArgumentException("User $userId does not exist"); - } - if ($absence->getId() === null) { - $persistedAbsence = $this->absenceMapper->insert($absence); - $this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent( - $persistedAbsence->toOutOufOfficeData($user) - )); - return $persistedAbsence; + $absence = $this->absenceMapper->insert($absence); + $eventData = $absence->toOutOufOfficeData( + $user, + $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(), + ); + $this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent($eventData)); + } else { + $absence = $this->absenceMapper->update($absence); + $eventData = $absence->toOutOufOfficeData( + $user, + $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(), + ); + $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData)); } - $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent( - $absence->toOutOufOfficeData($user) - )); - return $this->absenceMapper->update($absence); + $this->jobList->scheduleAfter( + OutOfOfficeEventDispatcherJob::class, + $eventData->getStartDate(), + [ + 'id' => $absence->getId(), + 'event' => OutOfOfficeEventDispatcherJob::EVENT_START, + ], + ); + $this->jobList->scheduleAfter( + OutOfOfficeEventDispatcherJob::class, + $eventData->getEndDate(), + [ + 'id' => $absence->getId(), + 'event' => OutOfOfficeEventDispatcherJob::EVENT_END, + ], + ); + + return $absence; } /** * @throws \OCP\DB\Exception */ - public function clearAbsence(string $userId): void { + public function clearAbsence(IUser $user): void { try { - $absence = $this->absenceMapper->findByUserId($userId); + $absence = $this->absenceMapper->findByUserId($user->getUID()); } catch (DoesNotExistException $e) { // Nothing to clear return; } $this->absenceMapper->delete($absence); - // TODO: this method should probably just take a IUser instance - $user = $this->userManager->get($userId); - if ($user === null) { - throw new InvalidArgumentException("User $userId does not exist"); - } - $eventData = $absence->toOutOufOfficeData($user); + $this->jobList->remove(OutOfOfficeEventDispatcherJob::class); + $eventData = $absence->toOutOufOfficeData( + $user, + $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(), + ); $this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData)); } } diff --git a/apps/dav/tests/integration/Db/PropertyMapperTest.php b/apps/dav/tests/integration/Db/PropertyMapperTest.php new file mode 100644 index 0000000000000..e14b22654409d --- /dev/null +++ b/apps/dav/tests/integration/Db/PropertyMapperTest.php @@ -0,0 +1,55 @@ + + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Tests\integration\Db; + +use OCA\DAV\Db\PropertyMapper; +use Test\TestCase; + +/** + * @group DB + */ +class PropertyMapperTest extends TestCase { + + /** @var PropertyMapper */ + private PropertyMapper $mapper; + + protected function setUp(): void { + parent::setUp(); + + $this->mapper = \OC::$server->get(PropertyMapper::class); + } + + public function testFindNonExistent(): void { + $props = $this->mapper->findPropertyByPathAndName( + 'userthatdoesnotexist', + 'path/that/does/not/exist/either', + 'nope', + ); + + self::assertEmpty($props); + } + +} diff --git a/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php new file mode 100644 index 0000000000000..ee2b69168bfba --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php @@ -0,0 +1,175 @@ + + * + * @author Richard Steinmetz + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob; +use OCA\DAV\CalDAV\TimezoneService; +use OCA\DAV\Db\Absence; +use OCA\DAV\Db\AbsenceMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class OutOfOfficeEventDispatcherJobTest extends TestCase { + private OutOfOfficeEventDispatcherJob $job; + + /** @var MockObject|ITimeFactory */ + private $timeFactory; + + /** @var MockObject|AbsenceMapper */ + private $absenceMapper; + + /** @var MockObject|LoggerInterface */ + private $logger; + + /** @var MockObject|IEventDispatcher */ + private $eventDispatcher; + + /** @var MockObject|IUserManager */ + private $userManager; + private MockObject|TimezoneService $timezoneService; + + protected function setUp(): void { + parent::setUp(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->absenceMapper = $this->createMock(AbsenceMapper::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->timezoneService = $this->createMock(TimezoneService::class); + + $this->job = new OutOfOfficeEventDispatcherJob( + $this->timeFactory, + $this->absenceMapper, + $this->logger, + $this->eventDispatcher, + $this->userManager, + $this->timezoneService, + ); + } + + public function testDispatchStartEvent() { + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); + + $absence = new Absence(); + $absence->setId(200); + $absence->setUserId('user'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->absenceMapper->expects(self::once()) + ->method('findById') + ->with(1) + ->willReturn($absence); + $this->userManager->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn($user); + $this->eventDispatcher->expects(self::once()) + ->method('dispatchTyped') + ->with(self::callback(static function ($event): bool { + self::assertInstanceOf(OutOfOfficeStartedEvent::class, $event); + return true; + })); + + $this->job->run([ + 'id' => 1, + 'event' => OutOfOfficeEventDispatcherJob::EVENT_START, + ]); + } + + public function testDispatchStopEvent() { + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); + + $absence = new Absence(); + $absence->setId(200); + $absence->setUserId('user'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->absenceMapper->expects(self::once()) + ->method('findById') + ->with(1) + ->willReturn($absence); + $this->userManager->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn($user); + $this->eventDispatcher->expects(self::once()) + ->method('dispatchTyped') + ->with(self::callback(static function ($event): bool { + self::assertInstanceOf(OutOfOfficeEndedEvent::class, $event); + return true; + })); + + $this->job->run([ + 'id' => 1, + 'event' => OutOfOfficeEventDispatcherJob::EVENT_END, + ]); + } + + public function testDoesntDispatchUnknownEvent() { + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); + + $absence = new Absence(); + $absence->setId(100); + $absence->setUserId('user'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->absenceMapper->expects(self::once()) + ->method('findById') + ->with(1) + ->willReturn($absence); + $this->userManager->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn($user); + $this->eventDispatcher->expects(self::never()) + ->method('dispatchTyped'); + $this->logger->expects(self::once()) + ->method('error'); + + $this->job->run([ + 'id' => 1, + 'event' => 'foobar', + ]); + } +} diff --git a/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php new file mode 100644 index 0000000000000..3646461dc4231 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php @@ -0,0 +1,161 @@ + + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +/* + * @copyright 2023 Christoph Wurst + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Tests\unit\CalDAV; + +use DateTimeZone; +use OCA\DAV\CalDAV\CalendarImpl; +use OCA\DAV\CalDAV\TimezoneService; +use OCA\DAV\Db\Property; +use OCA\DAV\Db\PropertyMapper; +use OCP\Calendar\ICalendar; +use OCP\Calendar\IManager; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VTimeZone; +use Test\TestCase; + +class TimezoneServiceTest extends TestCase { + + private IConfig|MockObject $config; + private PropertyMapper|MockObject $propertyMapper; + private IManager|MockObject $calendarManager; + private TimezoneService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->propertyMapper = $this->createMock(PropertyMapper::class); + $this->calendarManager = $this->createMock(IManager::class); + + $this->service = new TimezoneService( + $this->config, + $this->propertyMapper, + $this->calendarManager, + ); + } + + public function testGetUserTimezoneFromAvailability(): void { + $property = new Property(); + $property->setPropertyvalue('BEGIN:VCALENDAR +PRODID:Nextcloud DAV app +BEGIN:VTIMEZONE +TZID:Europe/Vienna +END:VTIMEZONE +END:VCALENDAR'); + $this->propertyMapper->expects(self::once()) + ->method('findPropertyByPathAndName') + ->willReturn([ + $property, + ]); + + $timezone = $this->service->getUserTimezone('test123'); + + self::assertNotNull($timezone); + self::assertEquals('Europe/Vienna', $timezone); + } + + public function testGetUserTimezoneFromPersonalCalendar(): void { + $this->config->expects(self::once()) + ->method('getUserValue') + ->with('test123', 'dav', 'defaultCalendar') + ->willReturn('personal-1'); + $other = $this->createMock(ICalendar::class); + $other->method('getUri')->willReturn('other'); + $personal = $this->createMock(CalendarImpl::class); + $personal->method('getUri')->willReturn('personal-1'); + $tz = new DateTimeZone('Europe/Berlin'); + $vtz = $this->createMock(VTimeZone::class); + $vtz->method('getTimeZone')->willReturn($tz); + $personal->method('getSchedulingTimezone')->willReturn($vtz); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with('principals/users/test123') + ->willReturn([ + $other, + $personal, + ]); + + $timezone = $this->service->getUserTimezone('test123'); + + self::assertNotNull($timezone); + self::assertEquals('Europe/Berlin', $timezone); + } + + public function testGetUserTimezoneFromAny(): void { + $this->config->expects(self::once()) + ->method('getUserValue') + ->with('test123', 'dav', 'defaultCalendar') + ->willReturn('personal-1'); + $other = $this->createMock(ICalendar::class); + $other->method('getUri')->willReturn('other'); + $personal = $this->createMock(CalendarImpl::class); + $personal->method('getUri')->willReturn('personal-2'); + $tz = new DateTimeZone('Europe/Prague'); + $vtz = $this->createMock(VTimeZone::class); + $vtz->method('getTimeZone')->willReturn($tz); + $personal->method('getSchedulingTimezone')->willReturn($vtz); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with('principals/users/test123') + ->willReturn([ + $other, + $personal, + ]); + + $timezone = $this->service->getUserTimezone('test123'); + + self::assertNotNull($timezone); + self::assertEquals('Europe/Prague', $timezone); + } + + public function testGetUserTimezoneNoneFound(): void { + $timezone = $this->service->getUserTimezone('test123'); + + self::assertNull($timezone); + } + +} diff --git a/config/config.sample.php b/config/config.sample.php index 27b99636a2207..a1f7332c4047b 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -228,6 +228,16 @@ */ 'force_locale' => 'en_US', +/** + * This sets the default timezone on your Nextcloud server, using IANA + * identifiers like ``Europe/Berlin`` or ``Pacific/Auckland``. The default + * timezone parameter is only used when the timezone of the user can't be + * determined. + * + * Defaults to ``UTC`` + */ +'default_timezone' => 'Europe/Berlin', + /** * ``true`` enables the Help menu item in the user menu (top right of the * Nextcloud Web interface). ``false`` removes the Help item. diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b90e2866bc6f0..78a101dcdf66c 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -742,7 +742,9 @@ 'OCP\\User\\Events\\BeforeUserLoggedOutEvent' => $baseDir . '/lib/public/User/Events/BeforeUserLoggedOutEvent.php', 'OCP\\User\\Events\\OutOfOfficeChangedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeChangedEvent.php', 'OCP\\User\\Events\\OutOfOfficeClearedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeClearedEvent.php', + 'OCP\\User\\Events\\OutOfOfficeEndedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeEndedEvent.php', 'OCP\\User\\Events\\OutOfOfficeScheduledEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeScheduledEvent.php', + 'OCP\\User\\Events\\OutOfOfficeStartedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeStartedEvent.php', 'OCP\\User\\Events\\PasswordUpdatedEvent' => $baseDir . '/lib/public/User/Events/PasswordUpdatedEvent.php', 'OCP\\User\\Events\\PostLoginEvent' => $baseDir . '/lib/public/User/Events/PostLoginEvent.php', 'OCP\\User\\Events\\UserChangedEvent' => $baseDir . '/lib/public/User/Events/UserChangedEvent.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index c1c3bc25869a5..2026dfec112ad 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -775,7 +775,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\User\\Events\\BeforeUserLoggedOutEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforeUserLoggedOutEvent.php', 'OCP\\User\\Events\\OutOfOfficeChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeChangedEvent.php', 'OCP\\User\\Events\\OutOfOfficeClearedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeClearedEvent.php', + 'OCP\\User\\Events\\OutOfOfficeEndedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeEndedEvent.php', 'OCP\\User\\Events\\OutOfOfficeScheduledEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeScheduledEvent.php', + 'OCP\\User\\Events\\OutOfOfficeStartedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeStartedEvent.php', 'OCP\\User\\Events\\PasswordUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PasswordUpdatedEvent.php', 'OCP\\User\\Events\\PostLoginEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PostLoginEvent.php', 'OCP\\User\\Events\\UserChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserChangedEvent.php', diff --git a/lib/private/User/AvailabilityCoordinator.php b/lib/private/User/AvailabilityCoordinator.php index 8e6b73bd56d94..e33a0aa155844 100644 --- a/lib/private/User/AvailabilityCoordinator.php +++ b/lib/private/User/AvailabilityCoordinator.php @@ -28,6 +28,7 @@ use JsonException; use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\TimezoneService; use OCA\DAV\Db\AbsenceMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\ICache; @@ -46,6 +47,7 @@ public function __construct( private AbsenceMapper $absenceMapper, private IConfig $config, private LoggerInterface $logger, + private TimezoneService $timezoneService, ) { $this->cache = $cacheFactory->createLocal('OutOfOfficeData'); } @@ -115,7 +117,10 @@ public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData { return null; } - $data = $absenceData->toOutOufOfficeData($user); + $data = $absenceData->toOutOufOfficeData( + $user, + $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(), + ); $this->setCachedOutOfOfficeData($data); return $data; } diff --git a/lib/public/User/Events/OutOfOfficeEndedEvent.php b/lib/public/User/Events/OutOfOfficeEndedEvent.php new file mode 100644 index 0000000000000..43a6bf77e28bd --- /dev/null +++ b/lib/public/User/Events/OutOfOfficeEndedEvent.php @@ -0,0 +1,51 @@ + + * + * @author Richard Steinmetz + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\User\Events; + +use OCP\EventDispatcher\Event; +use OCP\User\IOutOfOfficeData; + +/** + * Emitted when a user's out-of-office period ended + * + * @since 28.0.0 + */ +class OutOfOfficeEndedEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct(private IOutOfOfficeData $data) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getData(): IOutOfOfficeData { + return $this->data; + } +} diff --git a/lib/public/User/Events/OutOfOfficeStartedEvent.php b/lib/public/User/Events/OutOfOfficeStartedEvent.php new file mode 100644 index 0000000000000..f7816c968ddee --- /dev/null +++ b/lib/public/User/Events/OutOfOfficeStartedEvent.php @@ -0,0 +1,51 @@ + + * + * @author Richard Steinmetz + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\User\Events; + +use OCP\EventDispatcher\Event; +use OCP\User\IOutOfOfficeData; + +/** + * Emitted when a user's out-of-office period started + * + * @since 28.0.0 + */ +class OutOfOfficeStartedEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct(private IOutOfOfficeData $data) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getData(): IOutOfOfficeData { + return $this->data; + } +} diff --git a/tests/lib/User/AvailabilityCoordinatorTest.php b/tests/lib/User/AvailabilityCoordinatorTest.php index fd850fcdfd796..8a0b66181d23f 100644 --- a/tests/lib/User/AvailabilityCoordinatorTest.php +++ b/tests/lib/User/AvailabilityCoordinatorTest.php @@ -28,6 +28,7 @@ use OC\User\AvailabilityCoordinator; use OC\User\OutOfOfficeData; +use OCA\DAV\CalDAV\TimezoneService; use OCA\DAV\Db\Absence; use OCA\DAV\Db\AbsenceMapper; use OCP\ICache; @@ -45,6 +46,7 @@ class AvailabilityCoordinatorTest extends TestCase { private IConfig|MockObject $config; private AbsenceMapper $absenceMapper; private LoggerInterface $logger; + private MockObject|TimezoneService $timezoneService; protected function setUp(): void { parent::setUp(); @@ -54,6 +56,7 @@ protected function setUp(): void { $this->absenceMapper = $this->createMock(AbsenceMapper::class); $this->config = $this->createMock(IConfig::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->timezoneService = $this->createMock(TimezoneService::class); $this->cacheFactory->expects(self::once()) ->method('createLocal') @@ -64,6 +67,7 @@ protected function setUp(): void { $this->absenceMapper, $this->config, $this->logger, + $this->timezoneService, ); } @@ -86,6 +90,7 @@ public function testGetOutOfOfficeData(): void { $absence->setLastDay('2023-10-08'); $absence->setStatus('Vacation'); $absence->setMessage('On vacation'); + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); $user = $this->createMock(IUser::class); $user->method('getUID') @@ -101,13 +106,13 @@ public function testGetOutOfOfficeData(): void { ->willReturn($absence); $this->cache->expects(self::once()) ->method('set') - ->with('user', '{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}', 300); + ->with('user', '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation"}', 300); $expected = new OutOfOfficeData( '420', $user, - 1696118400, - 1696723200, + 1696111200, + 1696802340, 'Vacation', 'On vacation', ); @@ -149,6 +154,7 @@ public function testGetOutOfOfficeDataWithInvalidCachedData(): void { $absence->setLastDay('2023-10-08'); $absence->setStatus('Vacation'); $absence->setMessage('On vacation'); + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); $user = $this->createMock(IUser::class); $user->method('getUID') @@ -164,13 +170,13 @@ public function testGetOutOfOfficeDataWithInvalidCachedData(): void { ->willReturn($absence); $this->cache->expects(self::once()) ->method('set') - ->with('user', '{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}', 300); + ->with('user', '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation"}', 300); $expected = new OutOfOfficeData( '420', $user, - 1696118400, - 1696723200, + 1696111200, + 1696802340, 'Vacation', 'On vacation', );