Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(caldav): Create personal event for out-of-office messages #41340

Merged
merged 1 commit into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
'OCA\\DAV\\Listener\\CalendarShareUpdateListener' => $baseDir . '/../lib/Listener/CalendarShareUpdateListener.php',
'OCA\\DAV\\Listener\\CardListener' => $baseDir . '/../lib/Listener/CardListener.php',
'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => $baseDir . '/../lib/Listener/ClearPhotoCacheListener.php',
'OCA\\DAV\\Listener\\OutOfOfficeListener' => $baseDir . '/../lib/Listener/OutOfOfficeListener.php',
'OCA\\DAV\\Listener\\SubscriptionListener' => $baseDir . '/../lib/Listener/SubscriptionListener.php',
'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => $baseDir . '/../lib/Listener/TrustedServerRemovedListener.php',
'OCA\\DAV\\Listener\\UserPreferenceListener' => $baseDir . '/../lib/Listener/UserPreferenceListener.php',
Expand Down Expand Up @@ -316,6 +317,7 @@
'OCA\\DAV\\Search\\EventsSearchProvider' => $baseDir . '/../lib/Search/EventsSearchProvider.php',
'OCA\\DAV\\Search\\TasksSearchProvider' => $baseDir . '/../lib/Search/TasksSearchProvider.php',
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
'OCA\\DAV\\ServerFactory' => $baseDir . '/../lib/ServerFactory.php',
'OCA\\DAV\\Service\\AbsenceService' => $baseDir . '/../lib/Service/AbsenceService.php',
'OCA\\DAV\\Settings\\AvailabilitySettings' => $baseDir . '/../lib/Settings/AvailabilitySettings.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Listener\\CalendarShareUpdateListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarShareUpdateListener.php',
'OCA\\DAV\\Listener\\CardListener' => __DIR__ . '/..' . '/../lib/Listener/CardListener.php',
'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => __DIR__ . '/..' . '/../lib/Listener/ClearPhotoCacheListener.php',
'OCA\\DAV\\Listener\\OutOfOfficeListener' => __DIR__ . '/..' . '/../lib/Listener/OutOfOfficeListener.php',
'OCA\\DAV\\Listener\\SubscriptionListener' => __DIR__ . '/..' . '/../lib/Listener/SubscriptionListener.php',
'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => __DIR__ . '/..' . '/../lib/Listener/TrustedServerRemovedListener.php',
'OCA\\DAV\\Listener\\UserPreferenceListener' => __DIR__ . '/..' . '/../lib/Listener/UserPreferenceListener.php',
Expand Down Expand Up @@ -331,6 +332,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Search\\EventsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/EventsSearchProvider.php',
'OCA\\DAV\\Search\\TasksSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TasksSearchProvider.php',
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
'OCA\\DAV\\ServerFactory' => __DIR__ . '/..' . '/../lib/ServerFactory.php',
'OCA\\DAV\\Service\\AbsenceService' => __DIR__ . '/..' . '/../lib/Service/AbsenceService.php',
'OCA\\DAV\\Settings\\AvailabilitySettings' => __DIR__ . '/..' . '/../lib/Settings/AvailabilitySettings.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
Expand Down
8 changes: 8 additions & 0 deletions apps/dav/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
use OCA\DAV\Events\CardUpdatedEvent;
use OCA\DAV\Events\SubscriptionCreatedEvent;
use OCA\DAV\Events\SubscriptionDeletedEvent;
use OCA\DAV\Listener\OutOfOfficeListener;
use OCP\Accounts\UserUpdatedEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\Events\TrustedServerRemovedEvent;
Expand Down Expand Up @@ -103,6 +104,9 @@
use OCP\Contacts\IManager as IContactsManager;
use OCP\Files\AppData\IAppDataFactory;
use OCP\IUser;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeScheduledEvent;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
Expand Down Expand Up @@ -195,6 +199,10 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(BeforePreferenceDeletedEvent::class, UserPreferenceListener::class);
$context->registerEventListener(BeforePreferenceSetEvent::class, UserPreferenceListener::class);

$context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeListener::class);

$context->registerNotifierService(Notifier::class);

$context->registerCalendarProvider(CalendarProvider::class);
Expand Down
5 changes: 4 additions & 1 deletion apps/dav/lib/Db/Absence.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
namespace OCA\DAV\Db;

use DateTimeImmutable;
use Exception;
use InvalidArgumentException;
use JsonSerializable;
use OC\User\OutOfOfficeData;
Expand Down Expand Up @@ -70,8 +71,10 @@ public function toOutOufOfficeData(IUser $user): 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());
}
if ($this->getId() === null) {

Check notice

Code scanning / Psalm

DocblockTypeContradiction

int does not contain null
throw new Exception('Creating out-of-office data without ID');
}

//$user = $userManager->get($this->getUserId());
$startDate = new DateTimeImmutable($this->getFirstDay());
$endDate = new DateTimeImmutable($this->getLastDay());
return new OutOfOfficeData(
Expand Down
210 changes: 210 additions & 0 deletions apps/dav/lib/Listener/OutOfOfficeListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?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\Listener;

use DateTimeImmutable;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\ServerFactory;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeScheduledEvent;
use OCP\User\IOutOfOfficeData;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\NotFound;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\Reader;
use function fclose;
use function fopen;
use function fwrite;
use function rewind;

/**
* @template-implements IEventListener<OutOfOfficeScheduledEvent|OutOfOfficeChangedEvent|OutOfOfficeClearedEvent>
*/
class OutOfOfficeListener implements IEventListener {
public function __construct(private ServerFactory $serverFactory,
private IConfig $appConfig,
private LoggerInterface $logger) {
}

public function handle(Event $event): void {
if ($event instanceof OutOfOfficeScheduledEvent) {
$userId = $event->getData()->getUser()->getUID();
$principal = "principals/users/$userId";

$calendarNode = $this->getCalendarNode($principal, $userId);
if ($calendarNode === null) {
return;
}

$tz = $calendarNode->getProperties([])['{urn:ietf:params:xml:ns:caldav}calendar-timezone'] ?? null;
$vCalendarEvent = $this->createVCalendarEvent($event->getData(), $tz);
$stream = fopen('php://memory', 'rb+');
try {
fwrite($stream, $vCalendarEvent->serialize());
rewind($stream);
$calendarNode->createFile(
$this->getEventFileName($event->getData()->getId()),
$stream,
);
} finally {
fclose($stream);
}
} else if ($event instanceof OutOfOfficeChangedEvent) {
$userId = $event->getData()->getUser()->getUID();
$principal = "principals/users/$userId";

$calendarNode = $this->getCalendarNode($principal, $userId);
if ($calendarNode === null) {
return;
}
$tz = $calendarNode->getProperties([])['{urn:ietf:params:xml:ns:caldav}calendar-timezone'] ?? null;
$vCalendarEvent = $this->createVCalendarEvent($event->getData(), $tz);
try {
$oldEvent = $calendarNode->getChild($this->getEventFileName($event->getData()->getId()));
$oldEvent->put($vCalendarEvent->serialize());
return;
} catch (NotFound) {
$stream = fopen('php://memory', 'rb+');
try {
fwrite($stream, $vCalendarEvent->serialize());
rewind($stream);
$calendarNode->createFile(
$this->getEventFileName($event->getData()->getId()),
$stream,
);
} finally {
fclose($stream);
}
}
} else if ($event instanceof OutOfOfficeClearedEvent) {

Check notice

Code scanning / Psalm

RedundantConditionGivenDocblockType

Docblock-defined type OCP\User\Events\OutOfOfficeClearedEvent for $event is always OCP\User\Events\OutOfOfficeClearedEvent
$userId = $event->getData()->getUser()->getUID();
$principal = "principals/users/$userId";

$calendarNode = $this->getCalendarNode($principal, $userId);
if ($calendarNode === null) {
return;
}

try {
$oldEvent = $calendarNode->getChild($this->getEventFileName($event->getData()->getId()));
$oldEvent->delete();
} catch (NotFound) {
// The user must have deleted it or the default calendar changed -> ignore
}
}
}

private function getCalendarNode(string $principal, string $userId): ?Calendar {
$invitationServer = $this->serverFactory->createInviationResponseServer(false);
$server = $invitationServer->getServer();

/** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
$caldavPlugin = $server->getPlugin('caldav');
$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principal);
if ($calendarHomePath === null) {
$this->logger->debug('Principal has no calendar home path');
return null;
}
try {
/** @var CalendarHome $calendarHome */
$calendarHome = $server->tree->getNodeForPath($calendarHomePath);
} catch (NotFound $e) {
$this->logger->debug('Calendar home not found', [
'exception' => $e,
]);
return null;
}
$uri = $this->appConfig->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
try {
$calendarNode = $calendarHome->getChild($uri);
} catch (NotFound $e) {
$this->logger->debug('Personal calendar does not exist', [
'exception' => $e,
'uri' => $uri,
]);
return null;
}
if (!($calendarNode instanceof Calendar)) {
$this->logger->warning('Personal calendar node is not a calendar');
return null;
}
if ($calendarNode->isDeleted()) {
$this->logger->warning('Personal calendar has been deleted');
return null;
}

return $calendarNode;
}

private function getEventFileName(string $id): string {
return "out_of_office_$id.ics";
}

private function createVCalendarEvent(IOutOfOfficeData $data, ?string $timeZoneData): VCalendar {
$shortMessage = $data->getShortMessage();
$longMessage = $data->getMessage();
$start = (new DateTimeImmutable)
->setTimestamp($data->getStartDate())
->setTime(0, 0);
$end = (new DateTimeImmutable())
->setTimestamp($data->getEndDate())
->modify('+ 2 days')
->setTime(0, 0);
$vCalendar = new VCalendar();
$vCalendar->add('VEVENT', [
'SUMMARY' => $shortMessage,
'DESCRIPTION' => $longMessage,
'STATUS' => 'CONFIRMED',
'DTSTART' => $start,
'DTEND' => $end,
'X-NEXTCLOUD-OUT-OF-OFFICE' => $data->getId(),
]);
/** @var VEvent $vEvent */
$vEvent = $vCalendar->VEVENT;
if ($timeZoneData !== null) {
/** @var VCalendar $vtimezoneObj */
$vtimezoneObj = Reader::read($timeZoneData);
/** @var VTimeZone $vtimezone */
$vtimezone = $vtimezoneObj->VTIMEZONE;
$calendarTimeZone = $vtimezone->getTimeZone();
$vCalendar->add($vtimezone);

/** @psalm-suppress UndefinedMethod */
$vEvent->DTSTART->setDateTime($start->setTimezone($calendarTimeZone)->setTime(0, 0));

Check notice

Code scanning / Psalm

PossiblyNullReference

Cannot call method setDateTime on possibly null value
/** @psalm-suppress UndefinedMethod */
$vEvent->DTEND->setDateTime($end->setTimezone($calendarTimeZone)->setTime(0, 0));

Check notice

Code scanning / Psalm

PossiblyNullReference

Cannot call method setDateTime on possibly null value
}
return $vCalendar;
}
}
4 changes: 4 additions & 0 deletions apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,8 @@ private function requestIsForSubtree(array $subTrees): bool {
}
return false;
}

public function getSabreServer(): Connector\Sabre\Server {
return $this->server;
}
}
35 changes: 35 additions & 0 deletions apps/dav/lib/ServerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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;

use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;

class ServerFactory {

public function createInviationResponseServer(bool $public): InvitationResponseServer {
return new InvitationResponseServer(false);
}
}
12 changes: 8 additions & 4 deletions apps/dav/lib/Service/AbsenceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,18 @@ public function createOrUpdateAbsence(
if ($user === null) {
throw new InvalidArgumentException("User $userId does not exist");
}
$eventData = $absence->toOutOufOfficeData($user);

if ($absence->getId() === null) {
$this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent($eventData));
return $this->absenceMapper->insert($absence);
$persistedAbsence = $this->absenceMapper->insert($absence);
$this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent(
$persistedAbsence->toOutOufOfficeData($user)
));
return $persistedAbsence;
}

$this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData));
$this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent(
$absence->toOutOufOfficeData($user)
));
return $this->absenceMapper->update($absence);
}

Expand Down
Loading