diff options
Diffstat (limited to 'lib/private/Calendar')
-rw-r--r-- | lib/private/Calendar/AvailabilityResult.php | 28 | ||||
-rw-r--r-- | lib/private/Calendar/CalendarEventBuilder.php | 132 | ||||
-rw-r--r-- | lib/private/Calendar/Manager.php | 123 |
3 files changed, 272 insertions, 11 deletions
diff --git a/lib/private/Calendar/AvailabilityResult.php b/lib/private/Calendar/AvailabilityResult.php new file mode 100644 index 00000000000..8031758f64e --- /dev/null +++ b/lib/private/Calendar/AvailabilityResult.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Calendar; + +use OCP\Calendar\IAvailabilityResult; + +class AvailabilityResult implements IAvailabilityResult { + public function __construct( + private readonly string $attendee, + private readonly bool $available, + ) { + } + + public function getAttendeeEmail(): string { + return $this->attendee; + } + + public function isAvailable(): bool { + return $this->available; + } +} diff --git a/lib/private/Calendar/CalendarEventBuilder.php b/lib/private/Calendar/CalendarEventBuilder.php new file mode 100644 index 00000000000..9198d383ef9 --- /dev/null +++ b/lib/private/Calendar/CalendarEventBuilder.php @@ -0,0 +1,132 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Calendar; + +use DateTimeInterface; +use InvalidArgumentException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\ICalendarEventBuilder; +use OCP\Calendar\ICreateFromString; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; + +class CalendarEventBuilder implements ICalendarEventBuilder { + private ?DateTimeInterface $startDate = null; + private ?DateTimeInterface $endDate = null; + private ?string $summary = null; + private ?string $description = null; + private ?string $location = null; + private ?array $organizer = null; + private array $attendees = []; + + public function __construct( + private readonly string $uid, + private readonly ITimeFactory $timeFactory, + ) { + } + + public function setStartDate(DateTimeInterface $start): ICalendarEventBuilder { + $this->startDate = $start; + return $this; + } + + public function setEndDate(DateTimeInterface $end): ICalendarEventBuilder { + $this->endDate = $end; + return $this; + } + + public function setSummary(string $summary): ICalendarEventBuilder { + $this->summary = $summary; + return $this; + } + + public function setDescription(string $description): ICalendarEventBuilder { + $this->description = $description; + return $this; + } + + public function setLocation(string $location): ICalendarEventBuilder { + $this->location = $location; + return $this; + } + + public function setOrganizer(string $email, ?string $commonName = null): ICalendarEventBuilder { + $this->organizer = [$email, $commonName]; + return $this; + } + + public function addAttendee(string $email, ?string $commonName = null): ICalendarEventBuilder { + $this->attendees[] = [$email, $commonName]; + return $this; + } + + public function toIcs(): string { + if ($this->startDate === null) { + throw new InvalidArgumentException('Event is missing a start date'); + } + + if ($this->endDate === null) { + throw new InvalidArgumentException('Event is missing an end date'); + } + + if ($this->summary === null) { + throw new InvalidArgumentException('Event is missing a summary'); + } + + if ($this->organizer === null && $this->attendees !== []) { + throw new InvalidArgumentException('Event has attendees but is missing an organizer'); + } + + $vcalendar = new VCalendar(); + $props = [ + 'UID' => $this->uid, + 'DTSTAMP' => $this->timeFactory->now(), + 'SUMMARY' => $this->summary, + 'DTSTART' => $this->startDate, + 'DTEND' => $this->endDate, + ]; + if ($this->description !== null) { + $props['DESCRIPTION'] = $this->description; + } + if ($this->location !== null) { + $props['LOCATION'] = $this->location; + } + /** @var VEvent $vevent */ + $vevent = $vcalendar->add('VEVENT', $props); + if ($this->organizer !== null) { + self::addAttendeeToVEvent($vevent, 'ORGANIZER', $this->organizer); + } + foreach ($this->attendees as $attendee) { + self::addAttendeeToVEvent($vevent, 'ATTENDEE', $attendee); + } + return $vcalendar->serialize(); + } + + public function createInCalendar(ICreateFromString $calendar): string { + $fileName = $this->uid . '.ics'; + $calendar->createFromString($fileName, $this->toIcs()); + return $fileName; + } + + /** + * @param array{0: string, 1: ?string} $tuple A tuple of [$email, $commonName] where $commonName may be null. + */ + private static function addAttendeeToVEvent(VEvent $vevent, string $name, array $tuple): void { + [$email, $cn] = $tuple; + if (!str_starts_with($email, 'mailto:')) { + $email = "mailto:$email"; + } + $params = []; + if ($cn !== null) { + $params['CN'] = $cn; + } + $vevent->add($name, $email, $params); + } +} diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php index ba2124a5c23..e86e0e1d410 100644 --- a/lib/private/Calendar/Manager.php +++ b/lib/private/Calendar/Manager.php @@ -8,10 +8,14 @@ declare(strict_types=1); */ namespace OC\Calendar; +use DateTimeInterface; use OC\AppFramework\Bootstrap\Coordinator; +use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; +use OCA\DAV\ServerFactory; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Calendar\Exceptions\CalendarException; use OCP\Calendar\ICalendar; +use OCP\Calendar\ICalendarEventBuilder; use OCP\Calendar\ICalendarIsShared; use OCP\Calendar\ICalendarIsWritable; use OCP\Calendar\ICalendarProvider; @@ -19,10 +23,16 @@ use OCP\Calendar\ICalendarQuery; use OCP\Calendar\ICreateFromString; use OCP\Calendar\IHandleImipMessage; use OCP\Calendar\IManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Security\ISecureRandom; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Component\VFreeBusy; use Sabre\VObject\Property\VCard\DateTime; use Sabre\VObject\Reader; use Throwable; @@ -45,6 +55,9 @@ class Manager implements IManager { private ContainerInterface $container, private LoggerInterface $logger, private ITimeFactory $timeFactory, + private ISecureRandom $random, + private IUserManager $userManager, + private ServerFactory $serverFactory, ) { } @@ -216,21 +229,21 @@ class Manager implements IManager { string $recipient, string $calendarData, ): bool { - + $userCalendars = $this->getCalendarsForPrincipal($principalUri); if (empty($userCalendars)) { $this->logger->warning('iMip message could not be processed because user has no calendars'); return false; } - + /** @var VCalendar $vObject|null */ $calendarObject = Reader::read($calendarData); - + if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') { $this->logger->warning('iMip message contains an incorrect or invalid method'); return false; } - + if (!isset($calendarObject->VEVENT)) { $this->logger->warning('iMip message contains no event'); return false; @@ -242,12 +255,12 @@ class Manager implements IManager { $this->logger->warning('iMip message event dose not contains a UID'); return false; } - + if (!isset($eventObject->ATTENDEE)) { $this->logger->warning('iMip message event dose not contains any attendees'); return false; } - + foreach ($eventObject->ATTENDEE as $entry) { $address = trim(str_replace('mailto:', '', $entry->getValue())); if ($address === $recipient) { @@ -259,17 +272,17 @@ class Manager implements IManager { $this->logger->warning('iMip message event does not contain a attendee that matches the recipient'); return false; } - + foreach ($userCalendars as $calendar) { - + if (!$calendar instanceof ICalendarIsWritable && !$calendar instanceof ICalendarIsShared) { continue; } - + if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) { continue; } - + if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) { try { if ($calendar instanceof IHandleImipMessage) { @@ -282,7 +295,7 @@ class Manager implements IManager { } } } - + $this->logger->warning('iMip message event could not be processed because the no corresponding event was found in any calendar'); return false; } @@ -464,4 +477,92 @@ class Manager implements IManager { return false; } } + + public function createEventBuilder(): ICalendarEventBuilder { + $uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC); + return new CalendarEventBuilder($uid, $this->timeFactory); + } + + public function checkAvailability( + DateTimeInterface $start, + DateTimeInterface $end, + IUser $organizer, + array $attendees, + ): array { + $organizerMailto = 'mailto:' . $organizer->getEMailAddress(); + $request = new VCalendar(); + $request->METHOD = 'REQUEST'; + $request->add('VFREEBUSY', [ + 'DTSTART' => $start, + 'DTEND' => $end, + 'ORGANIZER' => $organizerMailto, + 'ATTENDEE' => $organizerMailto, + ]); + + $mailtoLen = strlen('mailto:'); + foreach ($attendees as $attendee) { + if (str_starts_with($attendee, 'mailto:')) { + $attendee = substr($attendee, $mailtoLen); + } + + $attendeeUsers = $this->userManager->getByEmail($attendee); + if ($attendeeUsers === []) { + continue; + } + + $request->VFREEBUSY->add('ATTENDEE', "mailto:$attendee"); + } + + $organizerUid = $organizer->getUID(); + $server = $this->serverFactory->createAttendeeAvailabilityServer(); + /** @var CustomPrincipalPlugin $plugin */ + $plugin = $server->getPlugin('auth'); + $plugin->setCurrentPrincipal("principals/users/$organizerUid"); + + $request = new Request( + 'POST', + "/calendars/$organizerUid/outbox/", + [ + 'Content-Type' => 'text/calendar', + 'Depth' => 0, + ], + $request->serialize(), + ); + $response = new Response(); + $server->invokeMethod($request, $response, false); + + $xmlService = new \Sabre\Xml\Service(); + $xmlService->elementMap = [ + '{urn:ietf:params:xml:ns:caldav}response' => 'Sabre\Xml\Deserializer\keyValue', + '{urn:ietf:params:xml:ns:caldav}recipient' => 'Sabre\Xml\Deserializer\keyValue', + ]; + $parsedResponse = $xmlService->parse($response->getBodyAsString()); + + $result = []; + foreach ($parsedResponse as $freeBusyResponse) { + $freeBusyResponse = $freeBusyResponse['value']; + if ($freeBusyResponse['{urn:ietf:params:xml:ns:caldav}request-status'] !== '2.0;Success') { + continue; + } + + $freeBusyResponseData = \Sabre\VObject\Reader::read( + $freeBusyResponse['{urn:ietf:params:xml:ns:caldav}calendar-data'] + ); + + $attendee = substr( + $freeBusyResponse['{urn:ietf:params:xml:ns:caldav}recipient']['{DAV:}href'], + $mailtoLen, + ); + + $vFreeBusy = $freeBusyResponseData->VFREEBUSY; + if (!($vFreeBusy instanceof VFreeBusy)) { + continue; + } + + // TODO: actually check values of FREEBUSY properties to find a free slot + $result[] = new AvailabilityResult($attendee, $vFreeBusy->isFree($start, $end)); + } + + return $result; + } } |