From 0cb241ac2bf3f0e319b531229a6845bd5b2cb946 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Sat, 7 Sep 2024 18:28:50 -0400 Subject: [PATCH] feat: add iMip Request Handling Signed-off-by: SebastianKrupinski --- apps/dav/lib/CalDAV/CalendarImpl.php | 14 ++ lib/private/Calendar/Manager.php | 74 +++++++ tests/lib/Calendar/ManagerTest.php | 290 ++++++++++++++++++++++++++- 3 files changed, 377 insertions(+), 1 deletion(-) diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index ea2ac0e9a62ac..e07cfc2038d69 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -132,6 +132,13 @@ public function getPermissions(): int { return $result; } + /** + * @since 31.0.0 + */ + public function isWritable(): bool { + return $this->calendar->canWrite(); + } + /** * @since 26.0.0 */ @@ -139,6 +146,13 @@ public function isDeleted(): bool { return $this->calendar->isDeleted(); } + /** + * @since 31.0.0 + */ + public function isShared(): bool { + return $this->calendar->isShared(); + } + /** * Create a new calendar event for this calendar * by way of an ICS string diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php index fa324273f5cb8..80644a9de51f0 100644 --- a/lib/private/Calendar/Manager.php +++ b/lib/private/Calendar/Manager.php @@ -204,6 +204,80 @@ public function newQuery(string $principalUri): ICalendarQuery { return new CalendarQuery($principalUri); } + /** + * @since 31.0.0 + * @throws \OCP\DB\Exception + */ + public function handleIMipRequest( + string $principalUri, + string $sender, + string $recipient, + string $calendarData, + ): bool { + // determine if user has any calendars + $userCalendars = $this->getCalendarsForPrincipal($principalUri); + if (empty($userCalendars)) { + $this->logger->warning('Could not find any calendars for principal ' . $principalUri); + return false; + } + // convert calendar string data to calendar object + /** @var VCalendar $vObject|null */ + $calendarObject = Reader::read($calendarData); + // determine if event has the correct method + if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') { + $this->logger->warning('iMip message event contains an incorrect or invalid method'); + return false; + } + // determine if calendar object contains any events + if (!isset($calendarObject->VEVENT)) { + $this->logger->warning('iMip message contains an no event'); + return false; + } + // extract event(s) + $eventObject = $calendarObject->VEVENT; + // determine if event contains a uid + if (!isset($eventObject->UID)) { + $this->logger->warning('iMip message event dose not contains a UID'); + return false; + } + // determine if event contains any attendees + if (!isset($eventObject->ATTENDEE)) { + $this->logger->warning('iMip message event dose not contains any attendees'); + return false; + } + // determine if any of the attendees are the recipient + foreach ($eventObject->ATTENDEE as $entry) { + $address = trim(str_replace('mailto:', '', $entry->getValue())); + if ($address === $recipient) { + $attendee = $address; + break; + } + } + if (!isset($attendee)) { + $this->logger->warning('iMip message event does not contain a attendee that matches the recipient'); + return false; + } + // find event in calendar and update it + foreach ($userCalendars as $calendar) { + // determine if calendar is deleted, shared, or read only and ignore + if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) { + continue; + } + // find event and update it + if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) { + try { + $calendar->handleIMipMessage('', $calendarData); // sabre will handle the scheduling behind the scenes + return true; + } catch (CalendarException $e) { + $this->logger->error('Could not update calendar for iMIP processing', ['exception' => $e]); + return false; + } + } + } + + return false; + } + /** * @throws \OCP\DB\Exception */ diff --git a/tests/lib/Calendar/ManagerTest.php b/tests/lib/Calendar/ManagerTest.php index 93fa21cf174f3..05855f7d5dc80 100644 --- a/tests/lib/Calendar/ManagerTest.php +++ b/tests/lib/Calendar/ManagerTest.php @@ -8,6 +8,7 @@ use OC\AppFramework\Bootstrap\Coordinator; use OC\Calendar\Manager; +use OCA\DAV\CalDAV\CalendarImpl; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Calendar\ICalendar; use OCP\Calendar\ICreateFromString; @@ -16,6 +17,7 @@ use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Sabre\VObject\Document; +use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Reader; use Test\TestCase; @@ -41,6 +43,8 @@ class ManagerTest extends TestCase { /** @var ITimeFactory|ITimeFactory&MockObject|MockObject */ private $time; + private VCalendar $vCalendar1a; + protected function setUp(): void { parent::setUp(); @@ -55,6 +59,22 @@ protected function setUp(): void { $this->logger, $this->time, ); + + // construct calendar with a 1 hour event and same start/end time zones + $this->vCalendar1a = new VCalendar(); + $vEvent = $this->vCalendar1a->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); } /** @@ -230,6 +250,274 @@ public function testIfEnabledIfSo() { $this->assertTrue($isEnabled); } + public function testHandleImipRequestWithNoCalendars(): void { + // construct calendar manager returns + /** @var Manager&MockObject $manager */ + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->coordinator, + $this->container, + $this->logger, + $this->time + ]) + ->setMethods([ + 'getCalendarsForPrincipal' + ]) + ->getMock(); + // construct logger returns + $this->logger->expects(self::once())->method('warning') + ->with('Could not find any calendars for principal principals/user/attendee1'); + // construct parameters + $principalUri = 'principals/user/attendee1'; + $sender = 'organizer@testing.com'; + $recipient = 'attendee1@testing.com'; + $calendar = $this->vCalendar1a; + $calendar->add('METHOD', 'REQUEST'); + // test method + $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize()); + + } + + public function testHandleImipRequestWithNoMethod(): void { + // construct mock user calendar + $userCalendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class); + // construct mock calendar manager and returns + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->coordinator, + $this->container, + $this->logger, + $this->time + ]) + ->setMethods([ + 'getCalendarsForPrincipal' + ]) + ->getMock(); + $manager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$userCalendar]); + // construct logger returns + $this->logger->expects(self::once())->method('warning') + ->with('iMip message event contains an incorrect or invalid method'); + // construct parameters + $principalUri = 'principals/user/attendee1'; + $sender = 'organizer@testing.com'; + $recipient = 'attendee1@testing.com'; + $calendar = $this->vCalendar1a; + // test method + $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize()); + + } + + public function testHandleImipRequestWithInvalidMethod(): void { + // construct mock user calendar + $userCalendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class); + // construct mock calendar manager and returns + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->coordinator, + $this->container, + $this->logger, + $this->time + ]) + ->setMethods([ + 'getCalendarsForPrincipal' + ]) + ->getMock(); + $manager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$userCalendar]); + // construct logger returns + $this->logger->expects(self::once())->method('warning') + ->with('iMip message event contains an incorrect or invalid method'); + // construct parameters + $principalUri = 'principals/user/attendee1'; + $sender = 'organizer@testing.com'; + $recipient = 'attendee1@testing.com'; + $calendar = $this->vCalendar1a; + $calendar->add('METHOD', 'CANCEL'); + // test method + $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize()); + + } + + public function testHandleImipRequestWithNoEvent(): void { + // construct mock user calendar + $userCalendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class); + // construct mock calendar manager and returns + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->coordinator, + $this->container, + $this->logger, + $this->time + ]) + ->setMethods([ + 'getCalendarsForPrincipal' + ]) + ->getMock(); + $manager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$userCalendar]); + // construct logger returns + $this->logger->expects(self::once())->method('warning') + ->with('iMip message contains an no event'); + // construct parameters + $principalUri = 'principals/user/attendee1'; + $sender = 'organizer@testing.com'; + $recipient = 'attendee1@testing.com'; + $calendar = $this->vCalendar1a; + $calendar->add('METHOD', 'REQUEST'); + $calendar->remove('VEVENT'); + // test method + $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize()); + + } + + public function testHandleImipRequestWithNoUid(): void { + // construct mock user calendar + $userCalendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class); + // construct mock calendar manager and returns + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->coordinator, + $this->container, + $this->logger, + $this->time + ]) + ->setMethods([ + 'getCalendarsForPrincipal' + ]) + ->getMock(); + $manager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$userCalendar]); + // construct logger returns + $this->logger->expects(self::once())->method('warning') + ->with('iMip message event dose not contains a UID'); + // construct parameters + $principalUri = 'principals/user/attendee1'; + $sender = 'organizer@testing.com'; + $recipient = 'attendee1@testing.com'; + $calendar = $this->vCalendar1a; + $calendar->add('METHOD', 'REQUEST'); + $calendar->VEVENT->remove('UID'); + // test method + $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize()); + + } + + public function testHandleImipRequestWithNoAttendee(): void { + // construct mock user calendar + $userCalendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class); + // construct mock calendar manager and returns + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->coordinator, + $this->container, + $this->logger, + $this->time + ]) + ->setMethods([ + 'getCalendarsForPrincipal' + ]) + ->getMock(); + $manager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$userCalendar]); + // construct logger returns + $this->logger->expects(self::once())->method('warning') + ->with('iMip message event dose not contains any attendees'); + // construct parameters + $principalUri = 'principals/user/attendee1'; + $sender = 'organizer@testing.com'; + $recipient = 'attendee1@testing.com'; + $calendar = $this->vCalendar1a; + $calendar->add('METHOD', 'REQUEST'); + $calendar->VEVENT->remove('ATTENDEE'); + // test method + $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize()); + + } + + public function testHandleImipRequestWithInvalidAttendee(): void { + // construct mock user calendar + $userCalendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class); + // construct mock calendar manager and returns + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->coordinator, + $this->container, + $this->logger, + $this->time + ]) + ->setMethods([ + 'getCalendarsForPrincipal' + ]) + ->getMock(); + $manager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$userCalendar]); + // construct logger returns + $this->logger->expects(self::once())->method('warning') + ->with('iMip message event does not contain a attendee that matches the recipient'); + // construct parameters + $principalUri = 'principals/user/attendee1'; + $sender = 'organizer@testing.com'; + $recipient = 'attendee2@testing.com'; + $calendar = $this->vCalendar1a; + $calendar->add('METHOD', 'REQUEST'); + // test method + $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize()); + + } + + public function testHandleImipRequest(): void { + // construct mock user calendar + $userCalendar = $this->createMock(CalendarImpl::class); + $userCalendar->expects(self::once()) + ->method('isDeleted') + ->willReturn(false); + $userCalendar->expects(self::once()) + ->method('isWritable') + ->willReturn(true); + $userCalendar->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $userCalendar->expects(self::once()) + ->method('search') + ->willReturn([['uri' => 'principals/user/attendee1/personal']]); + // construct mock calendar manager and returns + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->coordinator, + $this->container, + $this->logger, + $this->time + ]) + ->setMethods([ + 'getCalendarsForPrincipal' + ]) + ->getMock(); + $manager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$userCalendar]); + // construct parameters + $principalUri = 'principals/user/attendee1'; + $sender = 'organizer@testing.com'; + $recipient = 'attendee1@testing.com'; + $calendar = $this->vCalendar1a; + $calendar->add('METHOD', 'REQUEST'); + // construct user calendar returns + $userCalendar->expects(self::once()) + ->method('handleIMipMessage') + ->with('', $calendar->serialize()); + // test method + $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize()); + $this->assertTrue($result); + + } + public function testHandleImipReplyWrongMethod(): void { $principalUri = 'principals/user/linus'; $sender = 'pierre@general-store.com'; @@ -540,7 +828,7 @@ public function testHandleImipCancel(): void { $result = $manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData->serialize()); $this->assertTrue($result); } - + private function getVCalendarReply(): Document { $data = <<