aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Calendar
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Calendar')
-rw-r--r--lib/private/Calendar/AvailabilityResult.php28
-rw-r--r--lib/private/Calendar/CalendarEventBuilder.php147
-rw-r--r--lib/private/Calendar/CalendarQuery.php100
-rw-r--r--lib/private/Calendar/Manager.php627
-rw-r--r--lib/private/Calendar/Resource/Manager.php117
-rw-r--r--lib/private/Calendar/ResourcesRoomsUpdater.php413
-rw-r--r--lib/private/Calendar/Room/Manager.php124
7 files changed, 1556 insertions, 0 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..1aa11c2436d
--- /dev/null
+++ b/lib/private/Calendar/CalendarEventBuilder.php
@@ -0,0 +1,147 @@
+<?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\CalendarEventStatus;
+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 ?CalendarEventStatus $status = 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 setStatus(CalendarEventStatus $status): static {
+ $this->status = $status;
+ 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,
+ 'STATUS' => $this->status->value,
+ ];
+ 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;
+ if ($name === 'ORGANIZER') {
+ $params['ROLE'] = 'CHAIR';
+ $params['PARTSTAT'] = 'ACCEPTED';
+ } else {
+ $params['ROLE'] = 'REQ-PARTICIPANT';
+ $params['PARTSTAT'] = 'NEEDS-ACTION';
+ }
+ }
+ $vevent->add($name, $email, $params);
+ }
+}
diff --git a/lib/private/Calendar/CalendarQuery.php b/lib/private/Calendar/CalendarQuery.php
new file mode 100644
index 00000000000..4eb4a4cd636
--- /dev/null
+++ b/lib/private/Calendar/CalendarQuery.php
@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Calendar;
+
+use OCP\Calendar\ICalendarQuery;
+
+class CalendarQuery implements ICalendarQuery {
+ public array $searchProperties = [];
+
+ private ?string $searchPattern = null;
+
+ private array $options = [
+ 'types' => [],
+ ];
+
+ private ?int $offset = null;
+
+ private ?int $limit = null;
+
+ /** @var string[] */
+ private array $calendarUris = [];
+
+ public function __construct(
+ private string $principalUri,
+ ) {
+ }
+
+ public function getPrincipalUri(): string {
+ return $this->principalUri;
+ }
+
+ public function setPrincipalUri(string $principalUri): void {
+ $this->principalUri = $principalUri;
+ }
+
+ public function setSearchPattern(string $pattern): void {
+ $this->searchPattern = $pattern;
+ }
+
+ public function getSearchPattern(): ?string {
+ return $this->searchPattern;
+ }
+
+ public function addSearchProperty(string $value): void {
+ $this->searchProperties[] = $value;
+ }
+
+ public function getSearchProperties(): array {
+ return $this->searchProperties;
+ }
+
+ public function addSearchCalendar(string $calendarUri): void {
+ $this->calendarUris[] = $calendarUri;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getCalendarUris(): array {
+ return $this->calendarUris;
+ }
+
+ public function getLimit(): ?int {
+ return $this->limit;
+ }
+
+ public function setLimit(int $limit): void {
+ $this->limit = $limit;
+ }
+
+ public function getOffset(): ?int {
+ return $this->offset;
+ }
+
+ public function setOffset(int $offset): void {
+ $this->offset = $offset;
+ }
+
+ public function addType(string $value): void {
+ $this->options['types'][] = $value;
+ }
+
+ public function setTimerangeStart(\DateTimeImmutable $startTime): void {
+ $this->options['timerange']['start'] = $startTime;
+ }
+
+ public function setTimerangeEnd(\DateTimeImmutable $endTime): void {
+ $this->options['timerange']['end'] = $endTime;
+ }
+
+ public function getOptions(): array {
+ return $this->options;
+ }
+}
diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php
new file mode 100644
index 00000000000..7da1379809d
--- /dev/null
+++ b/lib/private/Calendar/Manager.php
@@ -0,0 +1,627 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+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;
+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\ParseException;
+use Sabre\VObject\Property\VCard\DateTime;
+use Sabre\VObject\Reader;
+use Throwable;
+use function array_map;
+use function array_merge;
+
+class Manager implements IManager {
+ /**
+ * @var ICalendar[] holds all registered calendars
+ */
+ private array $calendars = [];
+
+ /**
+ * @var \Closure[] to call to load/register calendar providers
+ */
+ private array $calendarLoaders = [];
+
+ public function __construct(
+ private Coordinator $coordinator,
+ private ContainerInterface $container,
+ private LoggerInterface $logger,
+ private ITimeFactory $timeFactory,
+ private ISecureRandom $random,
+ private IUserManager $userManager,
+ private ServerFactory $serverFactory,
+ ) {
+ }
+
+ /**
+ * This function is used to search and find objects within the user's calendars.
+ * In case $pattern is empty all events/journals/todos will be returned.
+ *
+ * @param string $pattern which should match within the $searchProperties
+ * @param array $searchProperties defines the properties within the query pattern should match
+ * @param array $options - optional parameters:
+ * ['timerange' => ['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 31.0.0
+ * @throws \OCP\DB\Exception
+ */
+ public function handleIMipRequest(
+ string $principalUri,
+ string $sender,
+ 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;
+ }
+
+ try {
+ /** @var VCalendar $vObject|null */
+ $calendarObject = Reader::read($calendarData);
+ } 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($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;
+ }
+
+ /** @var VEvent|null $vEvent */
+ $eventObject = $calendarObject->VEVENT;
+
+ if (!isset($eventObject->UID)) {
+ $this->logger->warning('iMip message event dose not contains a UID');
+ return false;
+ }
+
+ if (!isset($eventObject->ORGANIZER)) {
+ $this->logger->warning('iMip message event dose not contains an organizer');
+ 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) {
+ $attendee = $address;
+ break;
+ }
+ }
+ if (!isset($attendee)) {
+ $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) {
+ $calendar->handleIMipMessage('', $calendarData);
+ }
+ return true;
+ } catch (CalendarException $e) {
+ $this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
+ return false;
+ }
+ }
+ }
+
+ $this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar');
+ return false;
+ }
+
+ /**
+ * @throws \OCP\DB\Exception
+ */
+ public function handleIMipReply(
+ string $principalUri,
+ string $sender,
+ string $recipient,
+ string $calendarData,
+ ): bool {
+
+ $calendars = $this->getCalendarsForPrincipal($principalUri);
+ if (empty($calendars)) {
+ $this->logger->warning('iMip message could not be processed because user has no calendars');
+ return false;
+ }
+
+ try {
+ /** @var VCalendar $vObject|null */
+ $vObject = Reader::read($calendarData);
+ } 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 ($vObject === null) {
+ $this->logger->warning('iMip message contains an invalid calendar object');
+ return false;
+ }
+
+ if (!isset($vObject->METHOD) || $vObject->METHOD->getValue() !== 'REPLY') {
+ $this->logger->warning('iMip message contains an incorrect or invalid method');
+ return false;
+ }
+
+ if (!isset($vObject->VEVENT)) {
+ $this->logger->warning('iMip message contains no event');
+ return false;
+ }
+
+ /** @var VEvent|null $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;
+ }
+
+ // check if mail recipient and organizer are one and the same
+ $organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
+
+ if (strcasecmp($recipient, $organizer) !== 0) {
+ $this->logger->warning('iMip message event could not be processed because recipient and ORGANIZER must be identical');
+ return false;
+ }
+
+ //check if the event is in the future
+ /** @var DateTime $eventTime */
+ $eventTime = $vEvent->{'DTSTART'};
+ if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
+ $this->logger->warning('iMip message event could not be processed because the event is in the past');
+ return false;
+ }
+
+ $found = null;
+ // if the attendee has been found in at least one calendar event with the UID of the iMIP event
+ // we process it.
+ // Benefit: no attendee lost
+ // Drawback: attendees that have been deleted will still be able to update their partstat
+ foreach ($calendars as $calendar) {
+ // We should not search in writable calendars
+ if ($calendar instanceof IHandleImipMessage) {
+ $o = $calendar->search($sender, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
+ if (!empty($o)) {
+ $found = $calendar;
+ $name = $o[0]['uri'];
+ break;
+ }
+ }
+ }
+
+ if (empty($found)) {
+ $this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar', [
+ 'principalUri' => $principalUri,
+ 'eventUid' => $vEvent->{'UID'}->getValue(),
+ ]);
+ return false;
+ }
+
+ try {
+ $found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
+ } catch (CalendarException $e) {
+ $this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @since 25.0.0
+ * @throws \OCP\DB\Exception
+ */
+ public function handleIMipCancel(
+ string $principalUri,
+ string $sender,
+ ?string $replyTo,
+ string $recipient,
+ string $calendarData,
+ ): bool {
+
+ $calendars = $this->getCalendarsForPrincipal($principalUri);
+ if (empty($calendars)) {
+ $this->logger->warning('iMip message could not be processed because user has no calendars');
+ return false;
+ }
+
+ try {
+ /** @var VCalendar $vObject|null */
+ $vObject = Reader::read($calendarData);
+ } 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 ($vObject === null) {
+ $this->logger->warning('iMip message contains an invalid calendar object');
+ return false;
+ }
+
+ if (!isset($vObject->METHOD) || $vObject->METHOD->getValue() !== 'CANCEL') {
+ $this->logger->warning('iMip message contains an incorrect or invalid method');
+ return false;
+ }
+
+ if (!isset($vObject->VEVENT)) {
+ $this->logger->warning('iMip message contains no event');
+ return false;
+ }
+
+ /** @var VEvent|null $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;
+ }
+
+ $attendee = substr($vEvent->{'ATTENDEE'}->getValue(), 7);
+ if (strcasecmp($recipient, $attendee) !== 0) {
+ $this->logger->warning('iMip message event could not be processed because recipient must be an ATTENDEE of this event');
+ return false;
+ }
+
+ // Thirdly, we need to compare the email address the CANCEL is coming from (in Mail)
+ // or the Reply- To Address submitted with the CANCEL email
+ // to the email address in the ORGANIZER.
+ // We don't want to accept a CANCEL request from just anyone
+ $organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
+ $isNotOrganizer = ($replyTo !== null) ? (strcasecmp($sender, $organizer) !== 0 && strcasecmp($replyTo, $organizer) !== 0) : (strcasecmp($sender, $organizer) !== 0);
+ if ($isNotOrganizer) {
+ $this->logger->warning('iMip message event could not be processed because sender must be the ORGANIZER of this event');
+ return false;
+ }
+
+ //check if the event is in the future
+ /** @var DateTime $eventTime */
+ $eventTime = $vEvent->{'DTSTART'};
+ if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
+ $this->logger->warning('iMip message event could not be processed because the event is in the past');
+ return false;
+ }
+
+ $found = null;
+ // if the attendee has been found in at least one calendar event with the UID of the iMIP event
+ // we process it.
+ // Benefit: no attendee lost
+ // Drawback: attendees that have been deleted will still be able to update their partstat
+ foreach ($calendars as $calendar) {
+ // We should not search in writable calendars
+ if ($calendar instanceof IHandleImipMessage) {
+ $o = $calendar->search($recipient, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
+ if (!empty($o)) {
+ $found = $calendar;
+ $name = $o[0]['uri'];
+ break;
+ }
+ }
+ }
+
+ if (empty($found)) {
+ $this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar', [
+ 'principalUri' => $principalUri,
+ 'eventUid' => $vEvent->{'UID'}->getValue(),
+ ]);
+ return false;
+ }
+
+ try {
+ $found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
+ return true;
+ } catch (CalendarException $e) {
+ $this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
+ 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;
+ }
+}
diff --git a/lib/private/Calendar/Resource/Manager.php b/lib/private/Calendar/Resource/Manager.php
new file mode 100644
index 00000000000..db04e6a648a
--- /dev/null
+++ b/lib/private/Calendar/Resource/Manager.php
@@ -0,0 +1,117 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Calendar\Resource;
+
+use OC\AppFramework\Bootstrap\Coordinator;
+use OC\Calendar\ResourcesRoomsUpdater;
+use OCP\Calendar\Resource\IBackend;
+use OCP\Calendar\Resource\IManager;
+use OCP\IServerContainer;
+
+class Manager implements IManager {
+ private bool $bootstrapBackendsLoaded = false;
+
+ /**
+ * @var string[] holds all registered resource backends
+ * @psalm-var class-string<IBackend>[]
+ */
+ private array $backends = [];
+
+ /** @var IBackend[] holds all backends that have been initialized already */
+ private array $initializedBackends = [];
+
+ public function __construct(
+ private Coordinator $bootstrapCoordinator,
+ private IServerContainer $server,
+ private ResourcesRoomsUpdater $updater,
+ ) {
+ }
+
+ /**
+ * Registers a resource backend
+ *
+ * @since 14.0.0
+ */
+ public function registerBackend(string $backendClass): void {
+ $this->backends[$backendClass] = $backendClass;
+ }
+
+ /**
+ * Unregisters a resource backend
+ *
+ * @since 14.0.0
+ */
+ public function unregisterBackend(string $backendClass): void {
+ unset($this->backends[$backendClass], $this->initializedBackends[$backendClass]);
+ }
+
+ private function fetchBootstrapBackends(): void {
+ if ($this->bootstrapBackendsLoaded) {
+ return;
+ }
+
+ $context = $this->bootstrapCoordinator->getRegistrationContext();
+ if ($context === null) {
+ // Too soon
+ return;
+ }
+
+ foreach ($context->getCalendarResourceBackendRegistrations() as $registration) {
+ $this->backends[] = $registration->getService();
+ }
+ }
+
+ /**
+ * @return IBackend[]
+ * @throws \OCP\AppFramework\QueryException
+ * @since 14.0.0
+ */
+ public function getBackends():array {
+ $this->fetchBootstrapBackends();
+
+ foreach ($this->backends as $backend) {
+ if (isset($this->initializedBackends[$backend])) {
+ continue;
+ }
+
+ $this->initializedBackends[$backend] = $this->server->query($backend);
+ }
+
+ return array_values($this->initializedBackends);
+ }
+
+ /**
+ * @param string $backendId
+ * @throws \OCP\AppFramework\QueryException
+ */
+ public function getBackend($backendId): ?IBackend {
+ $backends = $this->getBackends();
+ foreach ($backends as $backend) {
+ if ($backend->getBackendIdentifier() === $backendId) {
+ return $backend;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * removes all registered backend instances
+ *
+ * @since 14.0.0
+ */
+ public function clear(): void {
+ $this->backends = [];
+ $this->initializedBackends = [];
+ }
+
+ public function update(): void {
+ $this->updater->updateResources();
+ }
+}
diff --git a/lib/private/Calendar/ResourcesRoomsUpdater.php b/lib/private/Calendar/ResourcesRoomsUpdater.php
new file mode 100644
index 00000000000..eacdaf0aeb4
--- /dev/null
+++ b/lib/private/Calendar/ResourcesRoomsUpdater.php
@@ -0,0 +1,413 @@
+<?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 OCA\DAV\CalDAV\CalDavBackend;
+use OCP\Calendar\BackendTemporarilyUnavailableException;
+use OCP\Calendar\IMetadataProvider;
+use OCP\Calendar\Resource\IBackend as IResourceBackend;
+use OCP\Calendar\Resource\IManager as IResourceManager;
+use OCP\Calendar\Resource\IResource;
+use OCP\Calendar\Room\IManager as IRoomManager;
+use OCP\Calendar\Room\IRoom;
+use OCP\IDBConnection;
+use Psr\Container\ContainerInterface;
+
+class ResourcesRoomsUpdater {
+ public function __construct(
+ private ContainerInterface $container,
+ private IDBConnection $dbConnection,
+ private CalDavBackend $calDavBackend,
+ ) {
+ }
+
+ /**
+ * Update resource cache from backends
+ */
+ public function updateResources(): void {
+ $this->updateFromBackend(
+ $this->container->get(IResourceManager::class),
+ 'calendar_resources',
+ 'calendar_resources_md',
+ 'resource_id',
+ 'principals/calendar-resources'
+ );
+ }
+
+ /**
+ * Update room cache from backends
+ */
+ public function updateRooms(): void {
+ $this->updateFromBackend(
+ $this->container->get(IRoomManager::class),
+ 'calendar_rooms',
+ 'calendar_rooms_md',
+ 'room_id',
+ 'principals/calendar-rooms'
+ );
+ }
+
+ /**
+ * Update cache from one specific backend manager, either ResourceManager or RoomManager
+ *
+ * @param IResourceManager|IRoomManager $backendManager
+ */
+ private function updateFromBackend($backendManager,
+ string $dbTable,
+ string $dbTableMetadata,
+ string $foreignKey,
+ string $principalPrefix): void {
+ $backends = $backendManager->getBackends();
+
+ foreach ($backends as $backend) {
+ $backendId = $backend->getBackendIdentifier();
+
+ try {
+ if ($backend instanceof IResourceBackend) {
+ $list = $backend->listAllResources();
+ } else {
+ $list = $backend->listAllRooms();
+ }
+ } catch (BackendTemporarilyUnavailableException $ex) {
+ continue;
+ }
+
+ $cachedList = $this->getAllCachedByBackend($dbTable, $backendId);
+ $newIds = array_diff($list, $cachedList);
+ $deletedIds = array_diff($cachedList, $list);
+ $editedIds = array_intersect($list, $cachedList);
+
+ foreach ($newIds as $newId) {
+ try {
+ if ($backend instanceof IResourceBackend) {
+ $resource = $backend->getResource($newId);
+ } else {
+ $resource = $backend->getRoom($newId);
+ }
+
+ $metadata = [];
+ if ($resource instanceof IMetadataProvider) {
+ $metadata = $this->getAllMetadataOfBackend($resource);
+ }
+ } catch (BackendTemporarilyUnavailableException $ex) {
+ continue;
+ }
+
+ $id = $this->addToCache($dbTable, $backendId, $resource);
+ $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata);
+ // we don't create the calendar here, it is created lazily
+ // when an event is actually scheduled with this resource / room
+ }
+
+ foreach ($deletedIds as $deletedId) {
+ $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId);
+ $this->deleteFromCache($dbTable, $id);
+ $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id);
+
+ $principalName = implode('-', [$backendId, $deletedId]);
+ $this->deleteCalendarDataForResource($principalPrefix, $principalName);
+ }
+
+ foreach ($editedIds as $editedId) {
+ $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId);
+
+ try {
+ if ($backend instanceof IResourceBackend) {
+ $resource = $backend->getResource($editedId);
+ } else {
+ $resource = $backend->getRoom($editedId);
+ }
+
+ $metadata = [];
+ if ($resource instanceof IMetadataProvider) {
+ $metadata = $this->getAllMetadataOfBackend($resource);
+ }
+ } catch (BackendTemporarilyUnavailableException $ex) {
+ continue;
+ }
+
+ $this->updateCache($dbTable, $id, $resource);
+
+ if ($resource instanceof IMetadataProvider) {
+ $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id);
+ $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata);
+ }
+ }
+ }
+ }
+
+ /**
+ * add entry to cache that exists remotely but not yet in cache
+ *
+ * @param string $table
+ * @param string $backendId
+ * @param IResource|IRoom $remote
+ *
+ * @return int Insert id
+ */
+ private function addToCache(string $table,
+ string $backendId,
+ $remote): int {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->insert($table)
+ ->values([
+ 'backend_id' => $query->createNamedParameter($backendId),
+ 'resource_id' => $query->createNamedParameter($remote->getId()),
+ 'email' => $query->createNamedParameter($remote->getEMail()),
+ 'displayname' => $query->createNamedParameter($remote->getDisplayName()),
+ 'group_restrictions' => $query->createNamedParameter(
+ $this->serializeGroupRestrictions(
+ $remote->getGroupRestrictions()
+ ))
+ ])
+ ->executeStatement();
+ return $query->getLastInsertId();
+ }
+
+ /**
+ * @param string $table
+ * @param string $foreignKey
+ * @param int $foreignId
+ * @param array $metadata
+ */
+ private function addMetadataToCache(string $table,
+ string $foreignKey,
+ int $foreignId,
+ array $metadata): void {
+ foreach ($metadata as $key => $value) {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->insert($table)
+ ->values([
+ $foreignKey => $query->createNamedParameter($foreignId),
+ 'key' => $query->createNamedParameter($key),
+ 'value' => $query->createNamedParameter($value),
+ ])
+ ->executeStatement();
+ }
+ }
+
+ /**
+ * delete entry from cache that does not exist anymore remotely
+ *
+ * @param string $table
+ * @param int $id
+ */
+ private function deleteFromCache(string $table,
+ int $id): void {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->delete($table)
+ ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
+ ->executeStatement();
+ }
+
+ /**
+ * @param string $table
+ * @param string $foreignKey
+ * @param int $id
+ */
+ private function deleteMetadataFromCache(string $table,
+ string $foreignKey,
+ int $id): void {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->delete($table)
+ ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
+ ->executeStatement();
+ }
+
+ /**
+ * update an existing entry in cache
+ *
+ * @param string $table
+ * @param int $id
+ * @param IResource|IRoom $remote
+ */
+ private function updateCache(string $table,
+ int $id,
+ $remote): void {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->update($table)
+ ->set('email', $query->createNamedParameter($remote->getEMail()))
+ ->set('displayname', $query->createNamedParameter($remote->getDisplayName()))
+ ->set('group_restrictions', $query->createNamedParameter(
+ $this->serializeGroupRestrictions(
+ $remote->getGroupRestrictions()
+ )))
+ ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
+ ->executeStatement();
+ }
+
+ /**
+ * @param string $dbTable
+ * @param string $foreignKey
+ * @param int $id
+ * @param array $metadata
+ * @param array $cachedMetadata
+ */
+ private function updateMetadataCache(string $dbTable,
+ string $foreignKey,
+ int $id,
+ array $metadata,
+ array $cachedMetadata): void {
+ $newMetadata = array_diff_key($metadata, $cachedMetadata);
+ $deletedMetadata = array_diff_key($cachedMetadata, $metadata);
+
+ foreach ($newMetadata as $key => $value) {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->insert($dbTable)
+ ->values([
+ $foreignKey => $query->createNamedParameter($id),
+ 'key' => $query->createNamedParameter($key),
+ 'value' => $query->createNamedParameter($value),
+ ])
+ ->executeStatement();
+ }
+
+ foreach ($deletedMetadata as $key => $value) {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->delete($dbTable)
+ ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
+ ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key)))
+ ->executeStatement();
+ }
+
+ $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata));
+ foreach ($existingKeys as $existingKey) {
+ if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->update($dbTable)
+ ->set('value', $query->createNamedParameter($metadata[$existingKey]))
+ ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
+ ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey)))
+ ->executeStatement();
+ }
+ }
+ }
+
+ /**
+ * serialize array of group restrictions to store them in database
+ *
+ * @param array $groups
+ *
+ * @return string
+ */
+ private function serializeGroupRestrictions(array $groups): string {
+ return \json_encode($groups, JSON_THROW_ON_ERROR);
+ }
+
+ /**
+ * Gets all metadata of a backend
+ *
+ * @param IResource|IRoom $resource
+ *
+ * @return array
+ */
+ private function getAllMetadataOfBackend($resource): array {
+ if (!($resource instanceof IMetadataProvider)) {
+ return [];
+ }
+
+ $keys = $resource->getAllAvailableMetadataKeys();
+ $metadata = [];
+ foreach ($keys as $key) {
+ $metadata[$key] = $resource->getMetadataForKey($key);
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * @param string $table
+ * @param string $foreignKey
+ * @param int $id
+ *
+ * @return array
+ */
+ private function getAllMetadataOfCache(string $table,
+ string $foreignKey,
+ int $id): array {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->select(['key', 'value'])
+ ->from($table)
+ ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)));
+ $result = $query->executeQuery();
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ $metadata = [];
+ foreach ($rows as $row) {
+ $metadata[$row['key']] = $row['value'];
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * Gets all cached rooms / resources by backend
+ *
+ * @param $tableName
+ * @param $backendId
+ *
+ * @return array
+ */
+ private function getAllCachedByBackend(string $tableName,
+ string $backendId): array {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->select('resource_id')
+ ->from($tableName)
+ ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)));
+ $result = $query->executeQuery();
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ return array_map(function ($row): string {
+ return $row['resource_id'];
+ }, $rows);
+ }
+
+ /**
+ * @param $principalPrefix
+ * @param $principalUri
+ */
+ private function deleteCalendarDataForResource(string $principalPrefix,
+ string $principalUri): void {
+ $calendar = $this->calDavBackend->getCalendarByUri(
+ implode('/', [$principalPrefix, $principalUri]),
+ CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI);
+
+ if ($calendar !== null) {
+ $this->calDavBackend->deleteCalendar(
+ $calendar['id'],
+ true // Because this wasn't deleted by a user
+ );
+ }
+ }
+
+ /**
+ * @param $table
+ * @param $backendId
+ * @param $resourceId
+ *
+ * @return int
+ */
+ private function getIdForBackendAndResource(string $table,
+ string $backendId,
+ string $resourceId): int {
+ $query = $this->dbConnection->getQueryBuilder();
+ $query->select('id')
+ ->from($table)
+ ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
+ ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
+ $result = $query->executeQuery();
+
+ $id = (int)$result->fetchOne();
+ $result->closeCursor();
+ return $id;
+ }
+}
diff --git a/lib/private/Calendar/Room/Manager.php b/lib/private/Calendar/Room/Manager.php
new file mode 100644
index 00000000000..65897010f2a
--- /dev/null
+++ b/lib/private/Calendar/Room/Manager.php
@@ -0,0 +1,124 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Calendar\Room;
+
+use OC\AppFramework\Bootstrap\Coordinator;
+use OC\Calendar\ResourcesRoomsUpdater;
+use OCP\Calendar\Room\IBackend;
+use OCP\Calendar\Room\IManager;
+use OCP\IServerContainer;
+
+class Manager implements IManager {
+ private bool $bootstrapBackendsLoaded = false;
+
+ /**
+ * @var string[] holds all registered resource backends
+ * @psalm-var class-string<IBackend>[]
+ */
+ private array $backends = [];
+
+ /** @var IBackend[] holds all backends that have been initialized already */
+ private array $initializedBackends = [];
+
+ public function __construct(
+ private Coordinator $bootstrapCoordinator,
+ private IServerContainer $server,
+ private ResourcesRoomsUpdater $updater,
+ ) {
+ }
+
+ /**
+ * Registers a resource backend
+ *
+ * @since 14.0.0
+ */
+ public function registerBackend(string $backendClass): void {
+ $this->backends[$backendClass] = $backendClass;
+ }
+
+ /**
+ * Unregisters a resource backend
+ *
+ * @param string $backendClass
+ * @since 14.0.0
+ */
+ public function unregisterBackend(string $backendClass): void {
+ unset($this->backends[$backendClass], $this->initializedBackends[$backendClass]);
+ }
+
+ private function fetchBootstrapBackends(): void {
+ if ($this->bootstrapBackendsLoaded) {
+ return;
+ }
+
+ $context = $this->bootstrapCoordinator->getRegistrationContext();
+ if ($context === null) {
+ // Too soon
+ return;
+ }
+
+ foreach ($context->getCalendarRoomBackendRegistrations() as $registration) {
+ $this->backends[] = $registration->getService();
+ }
+ }
+
+ /**
+ * @return IBackend[]
+ * @throws \OCP\AppFramework\QueryException
+ * @since 14.0.0
+ */
+ public function getBackends():array {
+ $this->fetchBootstrapBackends();
+
+ foreach ($this->backends as $backend) {
+ if (isset($this->initializedBackends[$backend])) {
+ continue;
+ }
+
+ /**
+ * @todo fetch from the app container
+ *
+ * The backend might have services injected that can't be build from the
+ * server container.
+ */
+ $this->initializedBackends[$backend] = $this->server->query($backend);
+ }
+
+ return array_values($this->initializedBackends);
+ }
+
+ /**
+ * @param string $backendId
+ * @throws \OCP\AppFramework\QueryException
+ */
+ public function getBackend($backendId): ?IBackend {
+ $backends = $this->getBackends();
+ foreach ($backends as $backend) {
+ if ($backend->getBackendIdentifier() === $backendId) {
+ return $backend;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * removes all registered backend instances
+ *
+ * @since 14.0.0
+ */
+ public function clear(): void {
+ $this->backends = [];
+ $this->initializedBackends = [];
+ }
+
+ public function update(): void {
+ $this->updater->updateRooms();
+ }
+}