From dcb17e3a1acacb82e59f80ae4de692a67613dd1c Mon Sep 17 00:00:00 2001 From: Richard Steinmetz Date: Sat, 11 Jan 2025 14:18:33 +0100 Subject: [PATCH] fixup! feat(ocp): add calendar api to retrieve availability of attendees --- lib/private/Calendar/Manager.php | 34 +++----- lib/public/Calendar/IManager.php | 6 +- tests/data/ics/free-busy-request.ics | 11 +-- tests/lib/Calendar/ManagerTest.php | 121 +++++++++++++++++---------- 4 files changed, 98 insertions(+), 74 deletions(-) diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php index 84488fca660e1..e86e0e1d41096 100644 --- a/lib/private/Calendar/Manager.php +++ b/lib/private/Calendar/Manager.php @@ -486,17 +486,17 @@ public function createEventBuilder(): ICalendarEventBuilder { public function checkAvailability( DateTimeInterface $start, DateTimeInterface $end, - IUser $user, + IUser $organizer, array $attendees, ): array { - $organizer = $user->getEMailAddress(); - + $organizerMailto = 'mailto:' . $organizer->getEMailAddress(); $request = new VCalendar(); $request->METHOD = 'REQUEST'; $request->add('VFREEBUSY', [ 'DTSTART' => $start, 'DTEND' => $end, - 'ORGANIZER' => "mailto:$organizer" + 'ORGANIZER' => $organizerMailto, + 'ATTENDEE' => $organizerMailto, ]); $mailtoLen = strlen('mailto:'); @@ -513,15 +513,15 @@ public function checkAvailability( $request->VFREEBUSY->add('ATTENDEE', "mailto:$attendee"); } - $userId = $user->getUID(); + $organizerUid = $organizer->getUID(); $server = $this->serverFactory->createAttendeeAvailabilityServer(); /** @var CustomPrincipalPlugin $plugin */ $plugin = $server->getPlugin('auth'); - $plugin->setCurrentPrincipal("principals/users/$userId"); + $plugin->setCurrentPrincipal("principals/users/$organizerUid"); $request = new Request( 'POST', - "/calendars/$userId/outbox/", + "/calendars/$organizerUid/outbox/", [ 'Content-Type' => 'text/calendar', 'Depth' => 0, @@ -554,27 +554,13 @@ public function checkAvailability( $mailtoLen, ); - $vfreebusy = $freeBusyResponseData->VFREEBUSY; - if (!($vfreebusy instanceof VFreeBusy)) { + $vFreeBusy = $freeBusyResponseData->VFREEBUSY; + if (!($vFreeBusy instanceof VFreeBusy)) { continue; } // TODO: actually check values of FREEBUSY properties to find a free slot - $isAvailable = true; - $freeBusyProps = $vfreebusy->FREEBUSY; - if ($freeBusyProps !== null) { - foreach ($freeBusyProps as $prop) { - // BUSY is the default, in case FBTYPE is not present - if (isset($prop['FBTYPE']) && $prop['FBTYPE'] === 'FREE') { - continue; - } - - $isAvailable = false; - break; - } - } - - $result[] = new AvailabilityResult($attendee, $isAvailable); + $result[] = new AvailabilityResult($attendee, $vFreeBusy->isFree($start, $end)); } return $result; diff --git a/lib/public/Calendar/IManager.php b/lib/public/Calendar/IManager.php index ddc454a08e01d..138dc60e78eda 100644 --- a/lib/public/Calendar/IManager.php +++ b/lib/public/Calendar/IManager.php @@ -174,14 +174,14 @@ public function createEventBuilder(): ICalendarEventBuilder; * * @since 31.0.0 * - * @param IUser $user The organizing user as which to do the availability check. - * @param string[] $attendees Email addresses of attendees to check for (with or without a "mailto:" prefix). Only users on this instance can be checked. The rest will be ignored. + * @param IUser $organizer The organizing user from whose perspective to do the availability check. + * @param string[] $attendees Email addresses of attendees to check for (with or without a "mailto:" prefix). Only users on this instance can be checked. The rest will be silently ignored. * @return IAvailabilityResult[] Availabilities of all attendees which are also users on this instance. As such, the array might not contain an entry for each given attendee. */ public function checkAvailability( DateTimeInterface $start, DateTimeInterface $end, - IUser $user, + IUser $organizer, array $attendees, ): array; } diff --git a/tests/data/ics/free-busy-request.ics b/tests/data/ics/free-busy-request.ics index 06b7dc1754843..dd01d35b67131 100644 --- a/tests/data/ics/free-busy-request.ics +++ b/tests/data/ics/free-busy-request.ics @@ -4,10 +4,11 @@ PRODID:-//Sabre//Sabre VObject 4.5.6//EN CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VFREEBUSY -DTSTART:20250109T082136Z -DTEND:20250109T102136Z -ORGANIZER:mailto:organizer@domain.tld -ATTENDEE:mailto:user.1@domain.tld -ATTENDEE:mailto:user.2@domain.tld +DTSTART:20250116T060000Z +DTEND:20250117T060000Z +ORGANIZER:mailto:admin@imap.localhost +ATTENDEE:mailto:admin@imap.localhost +ATTENDEE:mailto:user@imap.localhost +ATTENDEE:mailto:empty@imap.localhost END:VFREEBUSY END:VCALENDAR diff --git a/tests/lib/Calendar/ManagerTest.php b/tests/lib/Calendar/ManagerTest.php index 882e78d209668..12440bf7b4993 100644 --- a/tests/lib/Calendar/ManagerTest.php +++ b/tests/lib/Calendar/ManagerTest.php @@ -27,6 +27,7 @@ use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VFreeBusy; use Sabre\VObject\Document; use Sabre\VObject\Reader; use Test\TestCase; @@ -994,10 +995,10 @@ private function getVCalendarCancel(): Document { private function getFreeBusyResponse(): string { return << - + - mailto:user.1@domain.tld + mailto:admin@imap.localhost 2.0;Success BEGIN:VCALENDAR @@ -1006,20 +1007,21 @@ private function getFreeBusyResponse(): string { CALSCALE:GREGORIAN METHOD:REPLY BEGIN:VFREEBUSY -DTSTART:20250109T082136Z -DTEND:20250109T102136Z -DTSTAMP:20250109T082136Z -FREEBUSY::20250109T084500Z/20250109T090000Z -ATTENDEE:mailto:user.1@domain.tld -UID: -ORGANIZER:mailto:organizer@domain.tld +DTSTART:20250116T060000Z +DTEND:20250117T060000Z +DTSTAMP:20250111T125634Z +FREEBUSY:20250116T060000Z/20250116T230000Z +FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20250116T230000Z/20250117T060000Z +ATTENDEE:mailto:admin@imap.localhost +UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 +ORGANIZER;CN=admin:mailto:admin@imap.localhost END:VFREEBUSY END:VCALENDAR - mailto:user.2@domain.tld + mailto:empty@imap.localhost 2.0;Success BEGIN:VCALENDAR @@ -1028,16 +1030,45 @@ private function getFreeBusyResponse(): string { CALSCALE:GREGORIAN METHOD:REPLY BEGIN:VFREEBUSY -DTSTART:20250109T082136Z -DTEND:20250109T102136Z -DTSTAMP:20250109T082136Z -ATTENDEE:mailto:user.2@domain.tld -UID: -ORGANIZER:mailto:organizer@domain.tld +DTSTART:20250116T060000Z +DTEND:20250117T060000Z +DTSTAMP:20250111T125634Z +ATTENDEE:mailto:empty@imap.localhost +UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 +ORGANIZER;CN=admin:mailto:admin@imap.localhost END:VFREEBUSY END:VCALENDAR + + + mailto:user@imap.localhost + + 2.0;Success + BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.6//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VFREEBUSY +DTSTART:20250116T060000Z +DTEND:20250117T060000Z +DTSTAMP:20250111T125634Z +FREEBUSY:20250116T060000Z/20250116T230000Z +FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20250116T230000Z/20250117T060000Z +ATTENDEE:mailto:user@imap.localhost +UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 +ORGANIZER;CN=admin:mailto:admin@imap.localhost +END:VFREEBUSY +END:VCALENDAR + + + + + mailto:nouser@domain.tld + + 3.7;Could not find principal + EOF; } @@ -1046,25 +1077,26 @@ public function testCheckAvailability(): void { $organizer = $this->createMock(IUser::class); $organizer->expects(self::once()) ->method('getUID') - ->willReturn('organizer'); + ->willReturn('admin'); $organizer->expects(self::once()) ->method('getEMailAddress') - ->willReturn('organizer@domain.tld'); + ->willReturn('admin@imap.localhost'); $user1 = $this->createMock(IUser::class); $user2 = $this->createMock(IUser::class); - $this->userManager->expects(self::exactly(2)) + $this->userManager->expects(self::exactly(3)) ->method('getByEmail') ->willReturnMap([ - ['user.1@domain.tld', $user1], - ['user.2@domain.tld', $user2], + ['user@imap.localhost', [$user1]], + ['empty@imap.localhost', [$user2]], + ['nouser@domain.tld', []], ]); $authPlugin = $this->createMock(CustomPrincipalPlugin::class); $authPlugin->expects(self::once()) ->method('setCurrentPrincipal') - ->with('principals/users/organizer'); + ->with('principals/users/admin'); $server = $this->createMock(\OCA\DAV\Connector\Sabre\Server::class); $server->expects(self::once()) @@ -1080,7 +1112,7 @@ public function testCheckAvailability(): void { ) { $requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics'); $this->assertEquals('POST', $request->getMethod()); - $this->assertEquals('calendars/organizer/outbox', $request->getPath()); + $this->assertEquals('calendars/admin/outbox', $request->getPath()); $this->assertEquals('text/calendar', $request->getHeader('Content-Type')); $this->assertEquals('0', $request->getHeader('Depth')); $this->assertEquals($requestBody, $request->getBodyAsString()); @@ -1093,15 +1125,17 @@ public function testCheckAvailability(): void { ->method('createAttendeeAvailabilityServer') ->willReturn($server); - $start = new DateTimeImmutable('2025-01-09T08:21:36Z'); - $end = new DateTimeImmutable('2025-01-09T10:21:36Z'); + $start = new DateTimeImmutable('2025-01-16T06:00:00Z'); + $end = new DateTimeImmutable('2025-01-17T06:00:00Z'); $actual = $this->manager->checkAvailability($start, $end, $organizer, [ - 'user.1@domain.tld', - 'user.2@domain.tld', + 'user@imap.localhost', + 'empty@imap.localhost', + 'nouser@domain.tld', ]); $expected = [ - new AvailabilityResult('user.1@domain.tld', false), - new AvailabilityResult('user.2@domain.tld', true), + new AvailabilityResult('admin@imap.localhost', false), + new AvailabilityResult('empty@imap.localhost', true), + new AvailabilityResult('user@imap.localhost', false), ]; $this->assertEquals($expected, $actual); } @@ -1110,25 +1144,26 @@ public function testCheckAvailabilityWithMailtoPrefix(): void { $organizer = $this->createMock(IUser::class); $organizer->expects(self::once()) ->method('getUID') - ->willReturn('organizer'); + ->willReturn('admin'); $organizer->expects(self::once()) ->method('getEMailAddress') - ->willReturn('organizer@domain.tld'); + ->willReturn('admin@imap.localhost'); $user1 = $this->createMock(IUser::class); $user2 = $this->createMock(IUser::class); - $this->userManager->expects(self::exactly(2)) + $this->userManager->expects(self::exactly(3)) ->method('getByEmail') ->willReturnMap([ - ['user.1@domain.tld', $user1], - ['user.2@domain.tld', $user2], + ['user@imap.localhost', [$user1]], + ['empty@imap.localhost', [$user2]], + ['nouser@domain.tld', []], ]); $authPlugin = $this->createMock(CustomPrincipalPlugin::class); $authPlugin->expects(self::once()) ->method('setCurrentPrincipal') - ->with('principals/users/organizer'); + ->with('principals/users/admin'); $server = $this->createMock(\OCA\DAV\Connector\Sabre\Server::class); $server->expects(self::once()) @@ -1144,7 +1179,7 @@ public function testCheckAvailabilityWithMailtoPrefix(): void { ) { $requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics'); $this->assertEquals('POST', $request->getMethod()); - $this->assertEquals('calendars/organizer/outbox', $request->getPath()); + $this->assertEquals('calendars/admin/outbox', $request->getPath()); $this->assertEquals('text/calendar', $request->getHeader('Content-Type')); $this->assertEquals('0', $request->getHeader('Depth')); $this->assertEquals($requestBody, $request->getBodyAsString()); @@ -1157,15 +1192,17 @@ public function testCheckAvailabilityWithMailtoPrefix(): void { ->method('createAttendeeAvailabilityServer') ->willReturn($server); - $start = new DateTimeImmutable('2025-01-09T08:21:36Z'); - $end = new DateTimeImmutable('2025-01-09T10:21:36Z'); + $start = new DateTimeImmutable('2025-01-16T06:00:00Z'); + $end = new DateTimeImmutable('2025-01-17T06:00:00Z'); $actual = $this->manager->checkAvailability($start, $end, $organizer, [ - 'mailto:user.1@domain.tld', - 'mailto:user.2@domain.tld', + 'mailto:user@imap.localhost', + 'mailto:empty@imap.localhost', + 'mailto:nouser@domain.tld', ]); $expected = [ - new AvailabilityResult('user.1@domain.tld', false), - new AvailabilityResult('user.2@domain.tld', true), + new AvailabilityResult('admin@imap.localhost', false), + new AvailabilityResult('empty@imap.localhost', true), + new AvailabilityResult('user@imap.localhost', false), ]; $this->assertEquals($expected, $actual); }