aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorRichard Steinmetz <richard@steinmetz.cloud>2023-11-13 17:36:24 +0100
committerChristoph Wurst <christoph@winzerhof-wurst.at>2023-11-23 17:18:49 +0100
commit8191295f66cdea5da7854bfad01ad9540c4a55f4 (patch)
tree93fd27fe91ab0f40b4c765139a957b420806dedf /apps
parent953382e937a4085c1099449b29c40c7aab02fc3e (diff)
downloadnextcloud-server-8191295f66cdea5da7854bfad01ad9540c4a55f4.tar.gz
nextcloud-server-8191295f66cdea5da7854bfad01ad9540c4a55f4.zip
feat(dav): dispatch out-of-office started and ended events
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
Diffstat (limited to 'apps')
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php4
-rw-r--r--apps/dav/composer/composer/autoload_static.php4
-rw-r--r--apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php92
-rw-r--r--apps/dav/lib/CalDAV/TimezoneService.php98
-rw-r--r--apps/dav/lib/Controller/AvailabilitySettingsController.php15
-rw-r--r--apps/dav/lib/Db/Absence.php11
-rw-r--r--apps/dav/lib/Db/AbsenceMapper.php25
-rw-r--r--apps/dav/lib/Db/Property.php53
-rw-r--r--apps/dav/lib/Db/PropertyMapper.php57
-rw-r--r--apps/dav/lib/Service/AbsenceService.php75
-rw-r--r--apps/dav/tests/integration/Db/PropertyMapperTest.php55
-rw-r--r--apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php175
-rw-r--r--apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php161
13 files changed, 786 insertions, 39 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index 7891cad42eb..ef28133ed06 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -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',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index fdfe7feb6d9..73e680ef2e8 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 00000000000..9b219cf30da
--- /dev/null
+++ b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php
@@ -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
index 00000000000..bdbd0b9fe2c
--- /dev/null
+++ b/apps/dav/lib/CalDAV/TimezoneService.php
@@ -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');
+ }
+
+}
diff --git a/apps/dav/lib/Controller/AvailabilitySettingsController.php b/apps/dav/lib/Controller/AvailabilitySettingsController.php
index 3ff89fe87eb..3e10162dd84 100644
--- a/apps/dav/lib/Controller/AvailabilitySettingsController.php
+++ b/apps/dav/lib/Controller/AvailabilitySettingsController.php
@@ -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([]);
}
diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php
index 8de8ecc9aa0..3cd8037d57e 100644
--- a/apps/dav/lib/Db/Absence.php
+++ b/apps/dav/lib/Db/Absence.php
@@ -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,
diff --git a/apps/dav/lib/Db/AbsenceMapper.php b/apps/dav/lib/Db/AbsenceMapper.php
index 6e1133f779c..7529d04cf10 100644
--- a/apps/dav/lib/Db/AbsenceMapper.php
+++ b/apps/dav/lib/Db/AbsenceMapper.php
@@ -44,6 +44,31 @@ class AbsenceMapper extends QBMapper {
* @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
+ */
public function findByUserId(string $userId): Absence {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
diff --git a/apps/dav/lib/Db/Property.php b/apps/dav/lib/Db/Property.php
new file mode 100644
index 00000000000..5234ad852b1
--- /dev/null
+++ b/apps/dav/lib/Db/Property.php
@@ -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
index 00000000000..6f39d03d1c1
--- /dev/null
+++ b/apps/dav/lib/Db/PropertyMapper.php
@@ -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);
+ }
+
+}
diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php
index b50dd32e925..3f5168e386d 100644
--- a/apps/dav/lib/Service/AbsenceService.php
+++ b/apps/dav/lib/Service/AbsenceService.php
@@ -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
index 00000000000..e14b2265440
--- /dev/null
+++ b/apps/dav/tests/integration/Db/PropertyMapperTest.php
@@ -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
index 00000000000..ee2b69168bf
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php
@@ -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
index 00000000000..3646461dc42
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php
@@ -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);
+ }
+
+}