From 5b7a5474b9fe9bc08f65f48c115ea08ecac5a73c Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Wed, 8 Nov 2023 13:26:56 +0100 Subject: [PATCH] feat(caldav): Create personal event for out-of-office messages Signed-off-by: Christoph Wurst --- .../composer/composer/autoload_classmap.php | 2 + .../dav/composer/composer/autoload_static.php | 2 + apps/dav/lib/AppInfo/Application.php | 8 + apps/dav/lib/Db/Absence.php | 5 +- apps/dav/lib/Listener/OutOfOfficeListener.php | 210 +++++++ apps/dav/lib/Server.php | 4 + apps/dav/lib/ServerFactory.php | 35 ++ apps/dav/lib/Service/AbsenceService.php | 12 +- .../unit/Listener/OutOfOfficeListenerTest.php | 546 ++++++++++++++++++ 9 files changed, 819 insertions(+), 5 deletions(-) create mode 100644 apps/dav/lib/Listener/OutOfOfficeListener.php create mode 100644 apps/dav/lib/ServerFactory.php create mode 100644 apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 0bec456d1f109..7891cad42ebef 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -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', @@ -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', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 3c1891cf192ee..fdfe7feb6d908 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -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', @@ -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', diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 08529435caeb3..52f44ee02357a 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -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; @@ -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; @@ -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); diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php index e9ce1d2ea6496..8de8ecc9aa07e 100644 --- a/apps/dav/lib/Db/Absence.php +++ b/apps/dav/lib/Db/Absence.php @@ -27,6 +27,7 @@ namespace OCA\DAV\Db; use DateTimeImmutable; +use Exception; use InvalidArgumentException; use JsonSerializable; use OC\User\OutOfOfficeData; @@ -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) { + 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( diff --git a/apps/dav/lib/Listener/OutOfOfficeListener.php b/apps/dav/lib/Listener/OutOfOfficeListener.php new file mode 100644 index 0000000000000..645a01a35cfd0 --- /dev/null +++ b/apps/dav/lib/Listener/OutOfOfficeListener.php @@ -0,0 +1,210 @@ + + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\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 + */ +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) { + $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)); + /** @psalm-suppress UndefinedMethod */ + $vEvent->DTEND->setDateTime($end->setTimezone($calendarTimeZone)->setTime(0, 0)); + } + return $vCalendar; + } +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 15b97d028cc1a..4f00004fc8357 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -385,4 +385,8 @@ private function requestIsForSubtree(array $subTrees): bool { } return false; } + + public function getSabreServer(): Connector\Sabre\Server { + return $this->server; + } } diff --git a/apps/dav/lib/ServerFactory.php b/apps/dav/lib/ServerFactory.php new file mode 100644 index 0000000000000..7dc74f7d6ae34 --- /dev/null +++ b/apps/dav/lib/ServerFactory.php @@ -0,0 +1,35 @@ + + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV; + +use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; + +class ServerFactory { + + public function createInviationResponseServer(bool $public): InvitationResponseServer { + return new InvitationResponseServer(false); + } +} diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php index 69dee1bd8cc36..b31a910c4d212 100644 --- a/apps/dav/lib/Service/AbsenceService.php +++ b/apps/dav/lib/Service/AbsenceService.php @@ -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); } diff --git a/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php new file mode 100644 index 0000000000000..02227adfbfc4a --- /dev/null +++ b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php @@ -0,0 +1,546 @@ + + * + * @author 2023 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Tests\Unit\Listener; + +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\CalendarObject; +use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\CalDAV\Plugin; +use OCA\DAV\Connector\Sabre\Server; +use OCA\DAV\Listener\OutOfOfficeListener; +use OCA\DAV\ServerFactory; +use OCP\EventDispatcher\Event; +use OCP\IConfig; +use OCP\IUser; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\User\IOutOfOfficeData; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Tree; +use Test\TestCase; + +/** + * @covers \OCA\DAV\Listener\OutOfOfficeListener + */ +class OutOfOfficeListenerTest extends TestCase { + + private ServerFactory|MockObject $serverFactory; + private IConfig|MockObject $appConfig; + private LoggerInterface|MockObject $loggerInterface; + private OutOfOfficeListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->serverFactory = $this->createMock(ServerFactory::class); + $this->appConfig = $this->createMock(IConfig::class); + $this->loggerInterface = $this->createMock(LoggerInterface::class); + + $this->listener = new OutOfOfficeListener( + $this->serverFactory, + $this->appConfig, + $this->loggerInterface, + ); + } + + public function testHandleUnrelated(): void { + $event = new Event(); + + $this->listener->handle($event); + + $this->addToAssertionCount(1); + } + + public function testHandleSchedulingNoCalendarHome(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $event = new OutOfOfficeScheduledEvent($data); + + $this->listener->handle($event); + } + + public function testHandleSchedulingNoCalendarHomeNode(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeScheduledEvent($data); + + $this->listener->handle($event); + } + + public function testHandleSchedulingPersonalCalendarNotFound(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeScheduledEvent($data); + + $this->listener->handle($event); + } + + public function testHandleScheduling(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $calendar->expects(self::once()) + ->method('createFile'); + $event = new OutOfOfficeScheduledEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangeNoCalendarHome(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangeNoCalendarHomeNode(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangePersonalCalendarNotFound(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangeRecreate(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $calendar->expects(self::once()) + ->method('getChild') + ->willThrowException(new NotFound()); + $calendar->expects(self::once()) + ->method('createFile'); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChange(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $eventNode = $this->createMock(CalendarObject::class); + $calendar->expects(self::once()) + ->method('getChild') + ->willReturn($eventNode); + $eventNode->expects(self::once()) + ->method('put'); + $calendar->expects(self::never()) + ->method('createFile'); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClearNoCalendarHome(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClearNoCalendarHomeNode(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClearPersonalCalendarNotFound(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClearRecreate(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $calendar->expects(self::once()) + ->method('getChild') + ->willThrowException(new NotFound()); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClear(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $eventNode = $this->createMock(CalendarObject::class); + $calendar->expects(self::once()) + ->method('getChild') + ->willReturn($eventNode); + $eventNode->expects(self::once()) + ->method('delete'); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } +}