Skip to content

Commit a8a114c

Browse files
authored
Merge pull request #41701 from nextcloud/feat/dav/out-of-office-job-stable28
[stable28] feat(dav): dispatch out-of-office started and ended events
2 parents 33515da + d593206 commit a8a114c

20 files changed

Lines changed: 920 additions & 46 deletions

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php',
1919
'OCA\\DAV\\BackgroundJob\\EventReminderJob' => $baseDir . '/../lib/BackgroundJob/EventReminderJob.php',
2020
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
21+
'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => $baseDir . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php',
2122
'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => $baseDir . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php',
2223
'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => $baseDir . '/../lib/BackgroundJob/RefreshWebcalJob.php',
2324
'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => $baseDir . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php',
@@ -100,6 +101,7 @@
100101
'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => $baseDir . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php',
101102
'OCA\\DAV\\CalDAV\\Status\\Status' => $baseDir . '/../lib/CalDAV/Status/Status.php',
102103
'OCA\\DAV\\CalDAV\\Status\\StatusService' => $baseDir . '/../lib/CalDAV/Status/StatusService.php',
104+
'OCA\\DAV\\CalDAV\\TimezoneService' => $baseDir . '/../lib/CalDAV/TimezoneService.php',
103105
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php',
104106
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php',
105107
'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => $baseDir . '/../lib/CalDAV/Trashbin/Plugin.php',
@@ -210,6 +212,8 @@
210212
'OCA\\DAV\\Db\\AbsenceMapper' => $baseDir . '/../lib/Db/AbsenceMapper.php',
211213
'OCA\\DAV\\Db\\Direct' => $baseDir . '/../lib/Db/Direct.php',
212214
'OCA\\DAV\\Db\\DirectMapper' => $baseDir . '/../lib/Db/DirectMapper.php',
215+
'OCA\\DAV\\Db\\Property' => $baseDir . '/../lib/Db/Property.php',
216+
'OCA\\DAV\\Db\\PropertyMapper' => $baseDir . '/../lib/Db/PropertyMapper.php',
213217
'OCA\\DAV\\Direct\\DirectFile' => $baseDir . '/../lib/Direct/DirectFile.php',
214218
'OCA\\DAV\\Direct\\DirectHome' => $baseDir . '/../lib/Direct/DirectHome.php',
215219
'OCA\\DAV\\Direct\\Server' => $baseDir . '/../lib/Direct/Server.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ComposerStaticInitDAV
3333
'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php',
3434
'OCA\\DAV\\BackgroundJob\\EventReminderJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/EventReminderJob.php',
3535
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
36+
'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php',
3637
'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php',
3738
'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/RefreshWebcalJob.php',
3839
'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php',
@@ -115,6 +116,7 @@ class ComposerStaticInitDAV
115116
'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php',
116117
'OCA\\DAV\\CalDAV\\Status\\Status' => __DIR__ . '/..' . '/../lib/CalDAV/Status/Status.php',
117118
'OCA\\DAV\\CalDAV\\Status\\StatusService' => __DIR__ . '/..' . '/../lib/CalDAV/Status/StatusService.php',
119+
'OCA\\DAV\\CalDAV\\TimezoneService' => __DIR__ . '/..' . '/../lib/CalDAV/TimezoneService.php',
118120
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php',
119121
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php',
120122
'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/Plugin.php',
@@ -225,6 +227,8 @@ class ComposerStaticInitDAV
225227
'OCA\\DAV\\Db\\AbsenceMapper' => __DIR__ . '/..' . '/../lib/Db/AbsenceMapper.php',
226228
'OCA\\DAV\\Db\\Direct' => __DIR__ . '/..' . '/../lib/Db/Direct.php',
227229
'OCA\\DAV\\Db\\DirectMapper' => __DIR__ . '/..' . '/../lib/Db/DirectMapper.php',
230+
'OCA\\DAV\\Db\\Property' => __DIR__ . '/..' . '/../lib/Db/Property.php',
231+
'OCA\\DAV\\Db\\PropertyMapper' => __DIR__ . '/..' . '/../lib/Db/PropertyMapper.php',
228232
'OCA\\DAV\\Direct\\DirectFile' => __DIR__ . '/..' . '/../lib/Direct/DirectFile.php',
229233
'OCA\\DAV\\Direct\\DirectHome' => __DIR__ . '/..' . '/../lib/Direct/DirectHome.php',
230234
'OCA\\DAV\\Direct\\Server' => __DIR__ . '/..' . '/../lib/Direct/Server.php',
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
7+
*
8+
* @author Richard Steinmetz <richard@steinmetz.cloud>
9+
*
10+
* @license AGPL-3.0-or-later
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License as published by
14+
* the Free Software Foundation, either version 3 of the License, or
15+
* (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\DAV\BackgroundJob;
28+
29+
use OCA\DAV\CalDAV\TimezoneService;
30+
use OCA\DAV\Db\AbsenceMapper;
31+
use OCP\AppFramework\Db\DoesNotExistException;
32+
use OCP\AppFramework\Utility\ITimeFactory;
33+
use OCP\BackgroundJob\QueuedJob;
34+
use OCP\EventDispatcher\IEventDispatcher;
35+
use OCP\IUserManager;
36+
use OCP\User\Events\OutOfOfficeEndedEvent;
37+
use OCP\User\Events\OutOfOfficeStartedEvent;
38+
use Psr\Log\LoggerInterface;
39+
40+
class OutOfOfficeEventDispatcherJob extends QueuedJob {
41+
public const EVENT_START = 'start';
42+
public const EVENT_END = 'end';
43+
44+
public function __construct(
45+
ITimeFactory $time,
46+
private AbsenceMapper $absenceMapper,
47+
private LoggerInterface $logger,
48+
private IEventDispatcher $eventDispatcher,
49+
private IUserManager $userManager,
50+
private TimezoneService $timezoneService,
51+
) {
52+
parent::__construct($time);
53+
}
54+
55+
public function run($argument): void {
56+
$id = $argument['id'];
57+
$event = $argument['event'];
58+
59+
try {
60+
$absence = $this->absenceMapper->findById($id);
61+
} catch (DoesNotExistException | \OCP\DB\Exception $e) {
62+
$this->logger->error('Failed to dispatch out-of-office event: ' . $e->getMessage(), [
63+
'exception' => $e,
64+
'argument' => $argument,
65+
]);
66+
return;
67+
}
68+
69+
$userId = $absence->getUserId();
70+
$user = $this->userManager->get($userId);
71+
if ($user === null) {
72+
$this->logger->error("Failed to dispatch out-of-office event: User $userId does not exist", [
73+
'argument' => $argument,
74+
]);
75+
return;
76+
}
77+
78+
$data = $absence->toOutOufOfficeData(
79+
$user,
80+
$this->timezoneService->getUserTimezone($userId) ?? $this->timezoneService->getDefaultTimezone(),
81+
);
82+
if ($event === self::EVENT_START) {
83+
$this->eventDispatcher->dispatchTyped(new OutOfOfficeStartedEvent($data));
84+
} elseif ($event === self::EVENT_END) {
85+
$this->eventDispatcher->dispatchTyped(new OutOfOfficeEndedEvent($data));
86+
} else {
87+
$this->logger->error("Invalid out-of-office event: $event", [
88+
'argument' => $argument,
89+
]);
90+
}
91+
}
92+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
7+
*
8+
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*/
25+
26+
namespace OCA\DAV\CalDAV;
27+
28+
use OCA\DAV\Db\PropertyMapper;
29+
use OCP\Calendar\ICalendar;
30+
use OCP\Calendar\IManager;
31+
use OCP\IConfig;
32+
use Sabre\VObject\Component\VCalendar;
33+
use Sabre\VObject\Component\VTimeZone;
34+
use Sabre\VObject\Reader;
35+
use function array_reduce;
36+
37+
class TimezoneService {
38+
39+
public function __construct(private IConfig $config,
40+
private PropertyMapper $propertyMapper,
41+
private IManager $calendarManager) {
42+
}
43+
44+
public function getUserTimezone(string $userId): ?string {
45+
$availabilityPropPath = 'calendars/' . $userId . '/inbox';
46+
$availabilityProp = '{' . Plugin::NS_CALDAV . '}calendar-availability';
47+
$availabilities = $this->propertyMapper->findPropertyByPathAndName($userId, $availabilityPropPath, $availabilityProp);
48+
if (!empty($availabilities)) {
49+
$availability = $availabilities[0]->getPropertyvalue();
50+
/** @var VCalendar $vCalendar */
51+
$vCalendar = Reader::read($availability);
52+
/** @var VTimeZone $vTimezone */
53+
$vTimezone = $vCalendar->VTIMEZONE;
54+
// Sabre has a fallback to date_default_timezone_get
55+
return $vTimezone->getTimeZone()->getName();
56+
}
57+
58+
$principal = 'principals/users/' . $userId;
59+
$uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
60+
$calendars = $this->calendarManager->getCalendarsForPrincipal($principal);
61+
62+
/** @var ?VTimeZone $personalCalendarTimezone */
63+
$personalCalendarTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) use ($uri) {
64+
if ($acc !== null) {
65+
return $acc;
66+
}
67+
if ($calendar->getUri() === $uri && !$calendar->isDeleted() && $calendar instanceof CalendarImpl) {
68+
return $calendar->getSchedulingTimezone();
69+
}
70+
return null;
71+
});
72+
if ($personalCalendarTimezone !== null) {
73+
return $personalCalendarTimezone->getTimeZone()->getName();
74+
}
75+
76+
// No timezone in the personalCalendarTimezone calendar or no personalCalendarTimezone calendar
77+
// Loop through all calendars until we find a timezone.
78+
/** @var ?VTimeZone $firstTimezone */
79+
$firstTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) {
80+
if ($acc !== null) {
81+
return $acc;
82+
}
83+
if (!$calendar->isDeleted() && $calendar instanceof CalendarImpl) {
84+
return $calendar->getSchedulingTimezone();
85+
}
86+
return null;
87+
});
88+
if ($firstTimezone !== null) {
89+
return $firstTimezone->getTimeZone()->getName();
90+
}
91+
return null;
92+
}
93+
94+
public function getDefaultTimezone(): string {
95+
return $this->config->getSystemValueString('default_timezone', 'UTC');
96+
}
97+
98+
}

apps/dav/lib/Controller/AvailabilitySettingsController.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@
3535
use OCP\AppFramework\Http\JSONResponse;
3636
use OCP\AppFramework\Http\Response;
3737
use OCP\IRequest;
38+
use OCP\IUserSession;
3839

3940
class AvailabilitySettingsController extends Controller {
4041
public function __construct(
4142
IRequest $request,
42-
private ?string $userId,
43+
private ?IUserSession $userSession,
4344
private AbsenceService $absenceService,
4445
) {
4546
parent::__construct(Application::APP_ID, $request);
@@ -56,8 +57,8 @@ public function updateAbsence(
5657
string $status,
5758
string $message,
5859
): Response {
59-
$userId = $this->userId;
60-
if ($userId === null) {
60+
$user = $this->userSession?->getUser();
61+
if ($user === null) {
6162
return new JSONResponse([], Http::STATUS_FORBIDDEN);
6263
}
6364

@@ -68,7 +69,7 @@ public function updateAbsence(
6869
}
6970

7071
$absence = $this->absenceService->createOrUpdateAbsence(
71-
$userId,
72+
$user,
7273
$firstDay,
7374
$lastDay,
7475
$status,
@@ -82,12 +83,12 @@ public function updateAbsence(
8283
*/
8384
#[NoAdminRequired]
8485
public function clearAbsence(): Response {
85-
$userId = $this->userId;
86-
if ($userId === null) {
86+
$user = $this->userSession?->getUser();
87+
if ($user === null) {
8788
return new JSONResponse([], Http::STATUS_FORBIDDEN);
8889
}
8990

90-
$this->absenceService->clearAbsence($userId);
91+
$this->absenceService->clearAbsence($user);
9192
return new JSONResponse([]);
9293
}
9394

apps/dav/lib/Db/Absence.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626

2727
namespace OCA\DAV\Db;
2828

29-
use DateTimeImmutable;
29+
use DateTime;
30+
use DateTimeZone;
3031
use Exception;
3132
use InvalidArgumentException;
3233
use JsonSerializable;
@@ -67,16 +68,18 @@ public function __construct() {
6768
$this->addType('message', 'string');
6869
}
6970

70-
public function toOutOufOfficeData(IUser $user): IOutOfOfficeData {
71+
public function toOutOufOfficeData(IUser $user, string $timezone): IOutOfOfficeData {
7172
if ($user->getUID() !== $this->getUserId()) {
7273
throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ", got " . $user->getUID());
7374
}
7475
if ($this->getId() === null) {
7576
throw new Exception('Creating out-of-office data without ID');
7677
}
7778

78-
$startDate = new DateTimeImmutable($this->getFirstDay());
79-
$endDate = new DateTimeImmutable($this->getLastDay());
79+
$tz = new DateTimeZone($timezone);
80+
$startDate = new DateTime($this->getFirstDay(), $tz);
81+
$endDate = new DateTime($this->getLastDay(), $tz);
82+
$endDate->setTime(23, 59);
8083
return new OutOfOfficeData(
8184
(string)$this->getId(),
8285
$user,

apps/dav/lib/Db/AbsenceMapper.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,31 @@ public function __construct(IDBConnection $db) {
4040
parent::__construct($db, 'dav_absence', Absence::class);
4141
}
4242

43+
/**
44+
* @throws DoesNotExistException
45+
* @throws \OCP\DB\Exception
46+
*/
47+
public function findById(int $id): Absence {
48+
$qb = $this->db->getQueryBuilder();
49+
$qb->select('*')
50+
->from($this->getTableName())
51+
->where($qb->expr()->eq(
52+
'id',
53+
$qb->createNamedParameter($id, IQueryBuilder::PARAM_INT),
54+
IQueryBuilder::PARAM_INT),
55+
);
56+
try {
57+
return $this->findEntity($qb);
58+
} catch (MultipleObjectsReturnedException $e) {
59+
// Won't happen as id is the primary key
60+
throw new \RuntimeException(
61+
'The impossible has happened! The query returned multiple absence settings for one user.',
62+
0,
63+
$e,
64+
);
65+
}
66+
}
67+
4368
/**
4469
* @throws DoesNotExistException
4570
* @throws \OCP\DB\Exception

0 commit comments

Comments
 (0)