]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(dav): dispatch out-of-office started and ended events 41701/head
authorRichard Steinmetz <richard@steinmetz.cloud>
Mon, 13 Nov 2023 16:36:24 +0000 (17:36 +0100)
committerChristoph Wurst <christoph@winzerhof-wurst.at>
Thu, 23 Nov 2023 16:54:10 +0000 (17:54 +0100)
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
20 files changed:
apps/dav/composer/composer/autoload_classmap.php
apps/dav/composer/composer/autoload_static.php
apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php [new file with mode: 0644]
apps/dav/lib/CalDAV/TimezoneService.php [new file with mode: 0644]
apps/dav/lib/Controller/AvailabilitySettingsController.php
apps/dav/lib/Db/Absence.php
apps/dav/lib/Db/AbsenceMapper.php
apps/dav/lib/Db/Property.php [new file with mode: 0644]
apps/dav/lib/Db/PropertyMapper.php [new file with mode: 0644]
apps/dav/lib/Service/AbsenceService.php
apps/dav/tests/integration/Db/PropertyMapperTest.php [new file with mode: 0644]
apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php [new file with mode: 0644]
apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php [new file with mode: 0644]
config/config.sample.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/User/AvailabilityCoordinator.php
lib/public/User/Events/OutOfOfficeEndedEvent.php [new file with mode: 0644]
lib/public/User/Events/OutOfOfficeStartedEvent.php [new file with mode: 0644]
tests/lib/User/AvailabilityCoordinatorTest.php

index 7891cad42ebef784c13159f315c77aa682095d70..ef28133ed06d73e33db93cb90629b0337396b07c 100644 (file)
@@ -18,6 +18,7 @@ return array(
     '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 @@ return array(
     '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 @@ return array(
     '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',
index fdfe7feb6d90861b6534ef526d6cb755260816a8..73e680ef2e8971274a6a2b50b887db91ac27e924 100644 (file)
@@ -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 (file)
index 0000000..9b219cf
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0000000..bdbd0b9
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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');
+       }
+
+}
index 3ff89fe87eb66379a64358bdb7ed66c0b50681b9..3e10162dd842d37ac8f6b4446cd08c6dd1984d04 100644 (file)
@@ -35,11 +35,12 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired;
 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 @@ class AvailabilitySettingsController extends Controller {
                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 @@ class AvailabilitySettingsController extends Controller {
                }
 
                $absence = $this->absenceService->createOrUpdateAbsence(
-                       $userId,
+                       $user,
                        $firstDay,
                        $lastDay,
                        $status,
@@ -82,12 +83,12 @@ class AvailabilitySettingsController extends Controller {
         */
        #[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([]);
        }
 
index 8de8ecc9aa07e31d40ae956ed42d7dda2a5ae7f3..3cd8037d57e4ef3ebc3a272d82a76fea09236665 100644 (file)
@@ -26,7 +26,8 @@ declare(strict_types=1);
 
 namespace OCA\DAV\Db;
 
-use DateTimeImmutable;
+use DateTime;
+use DateTimeZone;
 use Exception;
 use InvalidArgumentException;
 use JsonSerializable;
@@ -67,7 +68,7 @@ class Absence extends Entity implements JsonSerializable {
                $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 @@ class Absence extends Entity implements JsonSerializable {
                        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,
index 6e1133f779c33c4734e7f96f737250ddcba47683..7529d04cf10406e4921edbe0219a72fbe992ee00 100644 (file)
@@ -40,6 +40,31 @@ class AbsenceMapper extends QBMapper {
                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 (file)
index 0000000..5234ad8
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..6f39d03
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\DAV\Db;
+
+use OCP\AppFramework\Db\QBMapper;
+use OCP\IDBConnection;
+
+/**
+ * @template-extends QBMapper<Property>
+ */
+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);
+       }
+
+}
index b50dd32e925fd44dbeba0eff039df712bd0c5bfb..3f5168e386d8e2d89b928339d42a1ced69530edc 100644 (file)
@@ -27,11 +27,14 @@ declare(strict_types=1);
 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 @@ class AbsenceService {
         * @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 (file)
index 0000000..e14b226
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..ee2b691
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0000000..3646461
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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);
+       }
+
+}
index 27b99636a2207b4fae302dbb3c8c8aa07239d821..a1f7332c4047beab53e5990b97b17e03cb45e615 100644 (file)
@@ -228,6 +228,16 @@ $CONFIG = [
  */
 '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.
index b90e2866bc6f0fa1ba6b29cdc957e86b0aa66255..78a101dcdf66c21c8e7ace0a44de0a9a1d2208ac 100644 (file)
@@ -742,7 +742,9 @@ return array(
     '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',
index c1c3bc25869a54f07e62b5abf9aa474930c45a3d..2026dfec112ad00ed33552c2af9a84ebeae78cb7 100644 (file)
@@ -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',
index 8e6b73bd56d94c1f902e41f0c6fcb761a5c48b75..e33a0aa1558444a316585a405c8e1ec816203d43 100644 (file)
@@ -28,6 +28,7 @@ namespace OC\User;
 
 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 @@ class AvailabilityCoordinator implements IAvailabilityCoordinator {
                private AbsenceMapper $absenceMapper,
                private IConfig $config,
                private LoggerInterface $logger,
+               private TimezoneService $timezoneService,
        ) {
                $this->cache = $cacheFactory->createLocal('OutOfOfficeData');
        }
@@ -115,7 +117,10 @@ class AvailabilityCoordinator implements IAvailabilityCoordinator {
                        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 (file)
index 0000000..43a6bf7
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0000000..f7816c9
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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;
+       }
+}
index fd850fcdfd7965a8d1ba39a00bbf9be07e03e20b..8a0b66181d23f4f2ae982d2f44e5e3ba462c0f97 100644 (file)
@@ -28,6 +28,7 @@ namespace Test\User;
 
 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 @@ class AvailabilityCoordinatorTest extends TestCase {
                $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 @@ class AvailabilityCoordinatorTest extends TestCase {
                        $this->absenceMapper,
                        $this->config,
                        $this->logger,
+                       $this->timezoneService,
                );
        }
 
@@ -86,6 +90,7 @@ class AvailabilityCoordinatorTest extends TestCase {
                $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 @@ class AvailabilityCoordinatorTest extends TestCase {
                        ->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 @@ class AvailabilityCoordinatorTest extends TestCase {
                $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 @@ class AvailabilityCoordinatorTest extends TestCase {
                        ->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',
                );