['start' => new DateTime(...), 'end' => new DateTime(...)]] * @param integer|null $limit - limit number of search results * @param integer|null $offset - offset for paging of search results * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs * @since 13.0.0 */ public function search( $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null, ): array { $this->loadCalendars(); $result = []; foreach ($this->calendars as $calendar) { $r = $calendar->search($pattern, $searchProperties, $options, $limit, $offset); foreach ($r as $o) { $o['calendar-key'] = $calendar->getKey(); $result[] = $o; } } return $result; } /** * Check if calendars are available * * @return bool true if enabled, false if not * @since 13.0.0 */ public function isEnabled(): bool { return !empty($this->calendars) || !empty($this->calendarLoaders); } /** * Registers a calendar * * @since 13.0.0 */ public function registerCalendar(ICalendar $calendar): void { $this->calendars[$calendar->getKey()] = $calendar; } /** * Unregisters a calendar * * @since 13.0.0 */ public function unregisterCalendar(ICalendar $calendar): void { unset($this->calendars[$calendar->getKey()]); } /** * In order to improve lazy loading a closure can be registered which will be called in case * calendars are actually requested * * @since 13.0.0 */ public function register(\Closure $callable): void { $this->calendarLoaders[] = $callable; } /** * @return ICalendar[] * * @since 13.0.0 */ public function getCalendars(): array { $this->loadCalendars(); return array_values($this->calendars); } /** * removes all registered calendar instances * * @since 13.0.0 */ public function clear(): void { $this->calendars = []; $this->calendarLoaders = []; } /** * loads all calendars */ private function loadCalendars(): void { foreach ($this->calendarLoaders as $callable) { $callable($this); } $this->calendarLoaders = []; } /** * @return ICreateFromString[] */ public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array { $context = $this->coordinator->getRegistrationContext(); if ($context === null) { return []; } return array_merge( ...array_map(function ($registration) use ($principalUri, $calendarUris) { try { /** @var ICalendarProvider $provider */ $provider = $this->container->get($registration->getService()); } catch (Throwable $e) { $this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [ 'exception' => $e, ]); return []; } return $provider->getCalendars($principalUri, $calendarUris); }, $context->getCalendarProviders()) ); } public function searchForPrincipal(ICalendarQuery $query): array { /** @var CalendarQuery $query */ $calendars = $this->getCalendarsForPrincipal( $query->getPrincipalUri(), $query->getCalendarUris(), ); $results = []; foreach ($calendars as $calendar) { $r = $calendar->search( $query->getSearchPattern() ?? '', $query->getSearchProperties(), $query->getOptions(), $query->getLimit(), $query->getOffset() ); foreach ($r as $o) { $o['calendar-key'] = $calendar->getKey(); $o['calendar-uri'] = $calendar->getUri(); $results[] = $o; } } return $results; } public function newQuery(string $principalUri): ICalendarQuery { return new CalendarQuery($principalUri); } /** * @since 32.0.0 * * @throws \OCP\DB\Exception */ public function handleIMip( string $userId, string $message, array $options = [], ): bool { $userUri = 'principals/users/' . $userId; $userCalendars = $this->getCalendarsForPrincipal($userUri); if (empty($userCalendars)) { $this->logger->warning('iMip message could not be processed because user has no calendars'); return false; } try { /** @var VCalendar $vObject|null */ $vObject = Reader::read($message); } catch (ParseException $e) { $this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]); return false; } if (!isset($vObject->VEVENT)) { $this->logger->warning('iMip message does not contain any event(s)'); return false; } /** @var VEvent $vEvent */ $vEvent = $vObject->VEVENT; if (!isset($vEvent->UID)) { $this->logger->warning('iMip message event dose not contains a UID'); return false; } if (!isset($vEvent->ORGANIZER)) { $this->logger->warning('iMip message event dose not contains an organizer'); return false; } if (!isset($vEvent->ATTENDEE)) { $this->logger->warning('iMip message event dose not contains any attendees'); return false; } foreach ($userCalendars as $calendar) { if (!$calendar instanceof ICalendarIsWritable) { continue; } if ($calendar->isDeleted() || !$calendar->isWritable()) { continue; } if (!empty($calendar->search('', [], ['uid' => $vEvent->UID->getValue()]))) { try { if ($calendar instanceof IHandleImipMessage) { $calendar->handleIMipMessage($userId, $vObject->serialize()); } return true; } catch (CalendarException $e) { $this->logger->error('iMip message could not be processed because an error occurred', ['exception' => $e]); return false; } } } if (isset($options['absent']) && $options['absent'] === 'create') { // retrieve the primary calendar for the user $calendar = $this->getPrimaryCalendar($userId); if ($calendar !== null && ( !$calendar instanceof IHandleImipMessage || !$calendar instanceof ICalendarIsWritable || $calendar->isDeleted() || !$calendar->isWritable() )) { $calendar = null; } // if no primary calendar is set, use the first writable calendar if ($calendar === null) { foreach ($userCalendars as $userCalendar) { if ($userCalendar instanceof IHandleImipMessage && $userCalendar instanceof ICalendarIsWritable && !$userCalendar->isDeleted() && $userCalendar->isWritable()) { $calendar = $userCalendar; break; } } } if ($calendar === null) { $this->logger->warning('iMip message could not be processed because no writable calendar was found'); return false; } $calendar->handleIMipMessage($userId, $vObject->serialize()); } $this->logger->warning('iMip message could not be processed because no corresponding event was found in any calendar'); return false; } /** * @since 31.0.0 * * @throws \OCP\DB\Exception */ public function handleIMipRequest( string $principalUri, string $sender, string $recipient, string $calendarData, ): bool { if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) { $this->logger->error('Invalid principal URI provided for iMip request'); return false; } $userId = substr($principalUri, 17); return $this->handleIMip($userId, $calendarData); } /** * @since 25.0.0 * * @throws \OCP\DB\Exception */ public function handleIMipReply( string $principalUri, string $sender, string $recipient, string $calendarData, ): bool { if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) { $this->logger->error('Invalid principal URI provided for iMip reply'); return false; } $userId = substr($principalUri, 17); return $this->handleIMip($userId, $calendarData); } /** * @since 25.0.0 * * @throws \OCP\DB\Exception */ public function handleIMipCancel( string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData, ): bool { if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) { $this->logger->error('Invalid principal URI provided for iMip cancel'); return false; } $userId = substr($principalUri, 17); return $this->handleIMip($userId, $calendarData); } 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; } public function getPrimaryCalendar(string $userId): ?ICalendar { // determine if the principal has a default calendar configured $properties = $this->propertyMapper->findPropertyByPathAndName( $userId, 'principals/users/' . $userId, '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' ); if ($properties === []) { return null; } // extract the calendar URI from the property value $propertyValue = $properties[0]->getPropertyvalue() ?? null; if (str_starts_with($propertyValue, 'calendars/' . $userId)) { $calendarUri = rtrim(str_replace('calendars/' . $userId . '/', '', $propertyValue), '/'); } if (empty($calendarUri)) { return null; } // retrieve the calendar by URI $calendars = $this->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarUri]); if ($calendars === []) { return null; } return $calendars[0]; } }