diff options
author | Richard Steinmetz <richard@steinmetz.cloud> | 2025-06-13 14:45:12 +0200 |
---|---|---|
committer | Richard Steinmetz <richard@steinmetz.cloud> | 2025-06-13 14:45:12 +0200 |
commit | 4fd1ccc62741c9a7f4fd005713a96894b80378cd (patch) | |
tree | 99d37a1c8dc11b1aa9900c42c11ceb576fd47f9b | |
parent | 2a21c74ab201d08d9fbd396d6e7becb71e0273e6 (diff) | |
download | nextcloud-server-feat/federated-calendar-sharing.tar.gz nextcloud-server-feat/federated-calendar-sharing.zip |
fixup! feat: calendar federationfeat/federated-calendar-sharing
28 files changed, 447 insertions, 239 deletions
diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php index 2cee1866a36..bd8dd5bf606 100644 --- a/apps/dav/appinfo/v1/caldav.php +++ b/apps/dav/appinfo/v1/caldav.php @@ -10,6 +10,7 @@ use OC\KnownUser\KnownUserService; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalendarRoot; use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Schedule\IMipPlugin; use OCA\DAV\CalDAV\Security\RateLimitingPlugin; @@ -71,6 +72,7 @@ $calDavBackend = new CalDavBackend( $dispatcher, $config, Server::get(\OCA\DAV\CalDAV\Sharing\Backend::class), + Server::get(FederatedCalendarMapper::class), true ); diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index a3bd18b55bf..9220bc41428 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -69,11 +69,11 @@ return array( 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendar' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendar.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarAuth' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarAuth.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarEntity' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarEntity.php', + 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarProvider' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarProvider.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarService' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarService.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php', - 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendar_actual' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendar_actual.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendar_old' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendar_old.php', 'OCA\\DAV\\CalDAV\\Federation\\FederationSharingPlugin' => $baseDir . '/../lib/CalDAV/Federation/FederationSharingPlugin.php', 'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => $baseDir . '/../lib/CalDAV/Federation/FederationSharingService.php', @@ -81,6 +81,7 @@ return array( 'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarProtocolParseException' => $baseDir . '/../lib/CalDAV/Federation/Protocol/CalendarProtocolParseException.php', 'OCA\\DAV\\CalDAV\\Federation\\Protocol\\ICalendarFederationProtocol' => $baseDir . '/../lib/CalDAV/Federation/Protocol/ICalendarFederationProtocol.php', 'OCA\\DAV\\CalDAV\\Federation\\RemoteUserCalendarHome' => $baseDir . '/../lib/CalDAV/Federation/RemoteUserCalendarHome.php', + 'OCA\\DAV\\CalDAV\\Federation\\RemoteUserPrincipalParser' => $baseDir . '/../lib/CalDAV/Federation/RemoteUserPrincipalParser.php', 'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', 'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index e6831f14345..ca971179eab 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -84,11 +84,11 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendar.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarAuth' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarAuth.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarEntity' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarEntity.php', + 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarProvider.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarService.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php', - 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendar_actual' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendar_actual.php', 'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendar_old' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendar_old.php', 'OCA\\DAV\\CalDAV\\Federation\\FederationSharingPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederationSharingPlugin.php', 'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederationSharingService.php', @@ -96,6 +96,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarProtocolParseException' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/Protocol/CalendarProtocolParseException.php', 'OCA\\DAV\\CalDAV\\Federation\\Protocol\\ICalendarFederationProtocol' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/Protocol/ICalendarFederationProtocol.php', 'OCA\\DAV\\CalDAV\\Federation\\RemoteUserCalendarHome' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/RemoteUserCalendarHome.php', + 'OCA\\DAV\\CalDAV\\Federation\\RemoteUserPrincipalParser' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/RemoteUserPrincipalParser.php', 'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', 'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php', diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 838674136ef..4d1b24e654f 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -11,6 +11,8 @@ use DateTimeImmutable; use DateTimeInterface; use Generator; use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity; +use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper; use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\DAV\Sharing\IShareable; @@ -210,6 +212,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription private IEventDispatcher $dispatcher, private IConfig $config, private Sharing\Backend $calendarSharingBackend, + private FederatedCalendarMapper $federatedCalendarMapper, private bool $legacyEndpoint = false, ) { } @@ -3750,4 +3753,21 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } }, $this->db); } + + public function getFederatedCalendarsForUser(string $principalUri): array { + return $this->atomic(function () use ($principalUri) { + $federatedCalendars = $this->federatedCalendarMapper->findByPrincipalUri($principalUri); + return array_map( + static fn (FederatedCalendarEntity $entity) => $entity->toCalendarInfo(), + $federatedCalendars, + ); + }, $this->db); + } + + public function getFederatedCalendarByUri(string $principalUri, string $uri): ?array { + return $this->atomic(function () use ($principalUri, $uri) { + $federatedCalendar = $this->federatedCalendarMapper->findByUri($principalUri, $uri); + return $federatedCalendar?->toCalendarInfo(); + }, $this->db); + } } diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php index ef36705fdd1..ffdebcf9072 100644 --- a/apps/dav/lib/CalDAV/CalendarHome.php +++ b/apps/dav/lib/CalDAV/CalendarHome.php @@ -8,12 +8,11 @@ namespace OCA\DAV\CalDAV; use OCA\DAV\AppInfo\PluginManager; -use OCA\DAV\CalDAV\Federation\FederatedCalendar_actual; +use OCA\DAV\CalDAV\Federation\FederatedCalendar; use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper; use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Integration\ICalendarProvider; use OCA\DAV\CalDAV\Trashbin\TrashbinHome; -use OCA\DAV\DAV\Sharing\Backend; use OCP\App\IAppManager; use OCP\IConfig; use OCP\IL10N; @@ -25,7 +24,6 @@ use Sabre\CalDAV\Backend\SchedulingSupport; use Sabre\CalDAV\Backend\SubscriptionSupport; use Sabre\CalDAV\Schedule\Inbox; use Sabre\CalDAV\Subscriptions\Subscription; -use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; use Sabre\DAV\Exception\MethodNotAllowed; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\INode; @@ -42,7 +40,7 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { /** @var PluginManager */ private $pluginManager; - private FederatedCalendarMapper $federatedCalendarMapper; + private readonly FederatedCalendarMapper $federatedCalendarMapper; private ?array $cachedChildren = null; public function __construct( @@ -97,27 +95,6 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { $objects[] = new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger); } - $federatedCalendars = $this->federatedCalendarMapper->findByPrincipalUri($this->principalInfo['uri']); - foreach ($federatedCalendars as $federatedCalendar) { - // TODO: move this to CalDavBackend? -> Ref subscriptions (rowToSubscription) - // TODO: move this to the the entity? - $calendarInfo = [ - 'id' => $federatedCalendar->getId(), - 'uri' => $federatedCalendar->getUri(), - 'principaluri' => $federatedCalendar->getPrincipaluri(), - 'federated' => true, - - '{DAV:}displayname' => $federatedCalendar->getDisplayName(), - // TODO: load from remote - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), - '{http://sabredav.org/ns}sync-token' => $federatedCalendar->getSyncToken(), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => $federatedCalendar->getSyncTokenForSabre(), - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $federatedCalendar->getSharedByPrincipal(), - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => $federatedCalendar->getPermissions() === Backend::ACCESS_READ, - ]; - $objects[] = new FederatedCalendar_actual($this->caldavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger, $this->federatedCalendarMapper); - } - if ($this->caldavBackend instanceof SchedulingSupport) { $objects[] = new Inbox($this->caldavBackend, $this->principalInfo['uri']); $objects[] = new Outbox($this->config, $this->principalInfo['uri']); @@ -130,6 +107,20 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { if ($this->caldavBackend instanceof CalDavBackend) { $objects[] = new TrashbinHome($this->caldavBackend, $this->principalInfo); + + $federatedCalendars = $this->caldavBackend->getFederatedCalendarsForUser( + $this->principalInfo['uri'], + ); + foreach ($federatedCalendars as $federatedCalendarInfo) { + $objects[] = new FederatedCalendar( + $this->caldavBackend, + $federatedCalendarInfo, + $this->l10n, + $this->config, + $this->logger, + $this->federatedCalendarMapper, + ); + } } // If the backend supports subscriptions, we'll add those as well, @@ -175,13 +166,29 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { return new TrashbinHome($this->caldavBackend, $this->principalInfo); } - // Calendar - this covers all "regular" calendars, but not shared - // only check if the method is available + // Only check if the methods are available if ($this->caldavBackend instanceof CalDavBackend) { + // Calendar - this covers all "regular" calendars, but not shared $calendar = $this->caldavBackend->getCalendarByUri($this->principalInfo['uri'], $name); if (!empty($calendar)) { return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger); } + + // Federated calendar + $federatedCalendar = $this->caldavBackend->getFederatedCalendarByUri( + $this->principalInfo['uri'], + $name, + ); + if ($federatedCalendar !== null) { + return new FederatedCalendar( + $this->caldavBackend, + $federatedCalendar, + $this->l10n, + $this->config, + $this->logger, + $this->federatedCalendarMapper, + ); + } } // Fallback to cover shared calendars @@ -203,26 +210,6 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { } } - $federatedCalendar = $this->federatedCalendarMapper->findByUri($this->principalInfo['uri'], $name); - if ($federatedCalendar) { - // TODO: merge with above - $calendarInfo = [ - 'id' => $federatedCalendar->getId(), - 'uri' => $federatedCalendar->getUri(), - 'principaluri' => $federatedCalendar->getPrincipaluri(), - 'federated' => true, - - '{DAV:}displayname' => $federatedCalendar->getDisplayName(), - // TODO: load from remote - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), - '{http://sabredav.org/ns}sync-token' => $federatedCalendar->getSyncToken(), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => $federatedCalendar->getSyncTokenForSabre(), - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $federatedCalendar->getSharedByPrincipal(), - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => $federatedCalendar->getPermissions() === Backend::ACCESS_READ, - ]; - return new FederatedCalendar_actual($this->caldavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger, $this->federatedCalendarMapper); - } - if (ExternalCalendar::isAppGeneratedCalendar($name)) { [$appId, $calendarUri] = ExternalCalendar::splitAppGeneratedCalendarUri($name); diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php index 3cc4039ed36..0f6ce928d38 100644 --- a/apps/dav/lib/CalDAV/CalendarProvider.php +++ b/apps/dav/lib/CalDAV/CalendarProvider.php @@ -8,6 +8,9 @@ declare(strict_types=1); */ namespace OCA\DAV\CalDAV; +use OCA\DAV\CalDAV\Federation\FederatedCalendar; +use OCA\DAV\CalDAV\Federation\FederatedCalendarImpl; +use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper; use OCA\DAV\Db\Property; use OCA\DAV\Db\PropertyMapper; use OCP\Calendar\ICalendarProvider; @@ -23,20 +26,27 @@ class CalendarProvider implements ICalendarProvider { private IConfig $config, private LoggerInterface $logger, private PropertyMapper $propertyMapper, + private FederatedCalendarMapper $federatedCalendarMapper, ) { } public function getCalendars(string $principalUri, array $calendarUris = []): array { $calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri) ?? []; + $federatedCalendarInfos = $this->calDavBackend->getFederatedCalendarsForUser($principalUri); if (!empty($calendarUris)) { $calendarInfos = array_filter($calendarInfos, function ($calendar) use ($calendarUris) { return in_array($calendar['uri'], $calendarUris); }); + + $federatedCalendarInfos = array_filter($federatedCalendarInfos, function ($federatedCalendar) use ($calendarUris) { + return in_array($federatedCalendar['uri'], $calendarUris); + }); } $iCalendars = []; + foreach ($calendarInfos as $calendarInfo) { $calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri'])); $calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger); @@ -46,6 +56,24 @@ class CalendarProvider implements ICalendarProvider { $this->calDavBackend, ); } + + foreach ($federatedCalendarInfos as $calendarInfo) { + $calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri'])); + $calendar = new FederatedCalendar( + $this->calDavBackend, + $calendarInfo, + $this->l10n, + $this->config, + $this->logger, + $this->federatedCalendarMapper, + ); + $iCalendars[] = new FederatedCalendarImpl( + $calendar, + $calendarInfo, + $this->calDavBackend, + ); + } + return $iCalendars; } diff --git a/apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php b/apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php index da011edb564..98a8eb60f4e 100644 --- a/apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php +++ b/apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php @@ -9,14 +9,12 @@ declare(strict_types=1); namespace OCA\DAV\CalDAV\Federation; -use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Federation\Protocol\CalendarFederationProtocolV1; use OCA\DAV\CalDAV\Federation\Protocol\ICalendarFederationProtocol; use OCP\AppFramework\Http; use OCP\Federation\Exceptions\ProviderCouldNotAddShareException; use OCP\Federation\ICloudFederationProvider; use OCP\Federation\ICloudFederationShare; -use OCP\Federation\ICloudIdManager; use Psr\Log\LoggerInterface; class CalendarFederationProvider implements ICloudFederationProvider { @@ -24,9 +22,7 @@ class CalendarFederationProvider implements ICloudFederationProvider { public const CALENDAR_RESOURCE = 'calendar'; public function __construct( - private readonly CalDavBackend $calDavBackend, private readonly LoggerInterface $logger, - private readonly ICloudIdManager $cloudIdManager, private readonly FederatedCalendarMapper $federatedCalendarMapper, ) { } @@ -38,11 +34,20 @@ class CalendarFederationProvider implements ICloudFederationProvider { public function shareReceived(ICloudFederationShare $share): string { if (!$this->isFederationEnabled()) { $this->logger->debug('Received a federation invite but federation is disabled'); - throw new ProviderCouldNotAddShareException('Server does not support talk federation', '', Http::STATUS_SERVICE_UNAVAILABLE); + throw new ProviderCouldNotAddShareException( + 'Server does not support talk federation', + '', + Http::STATUS_SERVICE_UNAVAILABLE, + ); } + if (!in_array($share->getShareType(), $this->getSupportedShareTypes(), true)) { $this->logger->debug('Received a federation invite for invalid share type'); - throw new ProviderCouldNotAddShareException('Support for sharing with non-users not implemented yet', '', Http::STATUS_NOT_IMPLEMENTED); + throw new ProviderCouldNotAddShareException( + 'Support for sharing with non-users not implemented yet', + '', + Http::STATUS_NOT_IMPLEMENTED, + ); // TODO: Implement group shares } @@ -50,7 +55,15 @@ class CalendarFederationProvider implements ICloudFederationProvider { // TODO: test what happens if no version in protocol switch ($rawProtocol[ICalendarFederationProtocol::PROP_VERSION]) { case CalendarFederationProtocolV1::VERSION: - $protocol = CalendarFederationProtocolV1::parse($rawProtocol); + try { + $protocol = CalendarFederationProtocolV1::parse($rawProtocol); + } catch (Protocol\CalendarProtocolParseException $e) { + throw new ProviderCouldNotAddShareException( + 'Invalid protocol data (v1)', + '', + Http::STATUS_BAD_REQUEST, + ); + } $calendarUrl = $protocol->getUrl(); $displayName = $protocol->getDisplayName(); $color = $protocol->getColor(); @@ -72,8 +85,9 @@ class CalendarFederationProvider implements ICloudFederationProvider { ); } - //$sharedBy = $this->cloudIdManager->resolveCloudId($share->getSharedBy()); - + // The calendar uri is the local name of the calendar. As such it must not contain slashes. + // Just use the hashed url for simplicity here. + // Example: calendars/foo-bar-user/<calendar-uri> $calendarUri = hash('md5', $calendarUrl); $sharedWithPrincipal = 'principals/users/' . $share->getShareWith(); @@ -95,7 +109,6 @@ class CalendarFederationProvider implements ICloudFederationProvider { $calendar->setRemoteUrl($calendarUrl); $calendar->setDisplayName($displayName); $calendar->setColor($color); - $calendar->setPermissions(1); // TODO: handle permissions $calendar->setToken($share->getShareSecret()); $calendar->setSharedBy($share->getSharedBy()); $calendar->setSharedByDisplayName($share->getSharedByDisplayName()); @@ -105,7 +118,6 @@ class CalendarFederationProvider implements ICloudFederationProvider { } public function notificationReceived($notificationType, $providerId, array $notification) { - // TODO: Implement notificationReceived() method. } /** diff --git a/apps/dav/lib/CalDAV/Federation/FederatedCalendar.php b/apps/dav/lib/CalDAV/Federation/FederatedCalendar.php index 8f87debdefa..34367e0e88e 100644 --- a/apps/dav/lib/CalDAV/Federation/FederatedCalendar.php +++ b/apps/dav/lib/CalDAV/Federation/FederatedCalendar.php @@ -10,82 +10,29 @@ declare(strict_types=1); namespace OCA\DAV\CalDAV\Federation; use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\DAV\Sharing\Backend; -use OCP\Calendar\ICalendar; -use OCP\Calendar\ICalendarIsShared; -use OCP\Calendar\ICalendarIsWritable; -use OCP\Calendar\IDeleteable; -use OCP\Constants; -use OCP\Server; +use OCA\DAV\CalDAV\Calendar; +use OCP\IConfig; +use OCP\IL10N; +use Psr\Log\LoggerInterface; +use Sabre\CalDAV\Backend; -class FederatedCalendar implements ICalendar, ICalendarIsShared, ICalendarIsWritable, IDeleteable { +class FederatedCalendar extends Calendar { public function __construct( - private int $id, - private string $uri, - private string $displayName, - private ?string $color, - private int $permissions, - private string $principalUri, + Backend\BackendInterface $caldavBackend, + $calendarInfo, + IL10N $l10n, + IConfig $config, + LoggerInterface $logger, + private readonly FederatedCalendarMapper $federatedCalendarMapper, ) { + parent::__construct($caldavBackend, $calendarInfo, $l10n, $config, $logger); } - public function getKey(): string { - return (string)$this->id; + public function delete() { + $this->federatedCalendarMapper->deleteById($this->getResourceId()); } - public function getUri(): string { - return $this->uri; - } - - public function getDisplayName(): ?string { - return $this->displayName; - } - - public function getDisplayColor(): ?string { - return $this->color; - } - - public function search(string $pattern, array $searchProperties = [], array $options = [], ?int $limit = null, ?int $offset = null): array { - // TODO: Implement search() method. - //return []; - $calDavBackend = Server::get(CalDavBackend::class); - $calendarInfo = [ - 'id' => $this->id, - 'principaluri' => $this->principalUri, - 'federated' => true, - '{http://owncloud.org/ns}owner-principal' => $this->principalUri, - ]; - $result = $calDavBackend->search($calendarInfo, $pattern, $searchProperties, $options, $limit, $offset); - - foreach ($result as $object) { - assert(count($object['objects']) === 1); - } - - $objects = array_map(static fn ($result) => $result['objects'][0], $result); - //$objects = array_map(static fn ($result) => ['VEVENT' => $result['objects'][0]], $result); - //return array_merge(...$objects); - return $objects; - } - - public function getPermissions(): int { - // TODO: implement this properly via ACLs? - return Constants::PERMISSION_READ; - } - - public function isDeleted(): bool { - return false; - } - - public function isShared(): bool { - return true; - } - - public function isWritable(): bool { - return false; - } - - public function delete(): void { - $federatedCalendarMapper = Server::get(FederatedCalendarMapper::class); - $federatedCalendarMapper->deleteById($this->id); + protected function getCalendarType(): int { + return CalDavBackend::CALENDAR_TYPE_FEDERATED; } } diff --git a/apps/dav/lib/CalDAV/Federation/FederatedCalendarEntity.php b/apps/dav/lib/CalDAV/Federation/FederatedCalendarEntity.php index 89ddfbfbe63..146d4b05d95 100644 --- a/apps/dav/lib/CalDAV/Federation/FederatedCalendarEntity.php +++ b/apps/dav/lib/CalDAV/Federation/FederatedCalendarEntity.php @@ -11,6 +11,7 @@ namespace OCA\DAV\CalDAV\Federation; use OCP\AppFramework\Db\Entity; use OCP\DB\Types; +use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; // TODO: write a migration for this entity @@ -37,6 +38,8 @@ use OCP\DB\Types; * @method void setSharedBy(string $sharedBy) * @method string getSharedByDisplayName() * @method void setSharedByDisplayName(string $sharedByDisplayName) + * @method string getComponents() + * @method void setComponents(string $components) */ class FederatedCalendarEntity extends Entity { protected string $principaluri = ''; @@ -50,6 +53,7 @@ class FederatedCalendarEntity extends Entity { protected ?int $lastSync = null; protected string $sharedBy = ''; protected string $sharedByDisplayName = ''; + protected string $components = ''; public function __construct() { $this->addType('principaluri', Types::STRING); @@ -63,17 +67,7 @@ class FederatedCalendarEntity extends Entity { $this->addType('lastSync', Types::INTEGER); $this->addType('sharedBy', Types::STRING); $this->addType('sharedByDisplayName', Types::STRING); - } - - public function toFederatedCalendar(): FederatedCalendar { - return new FederatedCalendar( - $this->getId(), - $this->getUri(), - $this->getDisplayName(), - $this->getColor(), - $this->getPermissions(), - $this->getPrincipaluri(), - ); + $this->addType('components', Types::STRING); } public function getSyncTokenForSabre(): string { @@ -83,4 +77,26 @@ class FederatedCalendarEntity extends Entity { public function getSharedByPrincipal(): string { return 'principals/remote-users/' . base64_encode($this->getSharedBy()); } + + public function getSupportedCalendarComponentSet(): SupportedCalendarComponentSet { + $components = explode(',', $this->getComponents()); + return new SupportedCalendarComponentSet($components); + } + + public function toCalendarInfo(): array { + return [ + 'id' => $this->getId(), + 'uri' => $this->getUri(), + 'principaluri' => $this->getPrincipaluri(), + //'federated' => true, + + '{DAV:}displayname' => $this->getDisplayName(), + '{http://sabredav.org/ns}sync-token' => $this->getSyncToken(), + '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $this->getSyncTokenForSabre(), + '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => $this->getSupportedCalendarComponentSet(), + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->getSharedByPrincipal(), + // TODO: implement read-write sharing + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => 1 + ]; + } } diff --git a/apps/dav/lib/CalDAV/Federation/FederatedCalendarImpl.php b/apps/dav/lib/CalDAV/Federation/FederatedCalendarImpl.php new file mode 100644 index 00000000000..20c238944d6 --- /dev/null +++ b/apps/dav/lib/CalDAV/Federation/FederatedCalendarImpl.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV\Federation; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\Calendar\ICalendar; +use OCP\Calendar\ICalendarIsEnabled; +use OCP\Calendar\ICalendarIsShared; +use OCP\Calendar\ICalendarIsWritable; +use OCP\Calendar\IDeleteable; +use OCP\Constants; + +class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, IDeleteable, ICalendarIsWritable, ICalendarIsEnabled { + public function __construct( + private readonly FederatedCalendar $federatedCalendar, + private readonly array $calendarInfo, + private readonly CalDavBackend $calDavBackend, + ) { + } + + public function getKey(): string { + return (string)$this->calendarInfo['id']; + } + + public function getUri(): string { + return $this->calendarInfo['uri']; + } + + public function getDisplayName(): ?string { + return $this->calendarInfo['{DAV:}displayname']; + } + + public function getDisplayColor(): ?string { + return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color']; + } + + public function search(string $pattern, array $searchProperties = [], array $options = [], ?int $limit = null, ?int $offset = null): array { + return $this->calDavBackend->search( + $this->calendarInfo, + $pattern, + $searchProperties, + $options, + $limit, + $offset, + ); + } + + public function getPermissions(): int { + // TODO: implement read-write sharing + return Constants::PERMISSION_READ; + } + + public function isDeleted(): bool { + return false; + } + + public function isShared(): bool { + return true; + } + + public function isWritable(): bool { + return false; + } + + public function isEnabled(): bool { + return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true; + } + + public function delete(): void { + $this->federatedCalendar->delete(); + } +} diff --git a/apps/dav/lib/CalDAV/Federation/FederatedCalendarMapper.php b/apps/dav/lib/CalDAV/Federation/FederatedCalendarMapper.php index f2736ebddf3..26f71781c35 100644 --- a/apps/dav/lib/CalDAV/Federation/FederatedCalendarMapper.php +++ b/apps/dav/lib/CalDAV/Federation/FederatedCalendarMapper.php @@ -62,6 +62,9 @@ class FederatedCalendarMapper extends QBMapper { return $this->findEntity($qb); } catch (DoesNotExistException $e) { return null; + } catch (MultipleObjectsReturnedException $e) { + // Should never happen + return null; } } diff --git a/apps/dav/lib/CalDAV/Federation/FederatedCalendarProvider.php b/apps/dav/lib/CalDAV/Federation/FederatedCalendarProvider.php index e6fa8f73edc..b7f13654287 100644 --- a/apps/dav/lib/CalDAV/Federation/FederatedCalendarProvider.php +++ b/apps/dav/lib/CalDAV/Federation/FederatedCalendarProvider.php @@ -23,7 +23,7 @@ class FederatedCalendarProvider implements ICalendarProvider { if (!empty($calendarUris)) { $calendars = array_filter( $calendars, - static fn(FederatedCalendar $calendar) => in_array( + static fn(FederatedCalendarImpl $calendar) => in_array( $calendar->getUri(), $calendarUris, true, diff --git a/apps/dav/lib/CalDAV/Federation/FederatedCalendarService.php b/apps/dav/lib/CalDAV/Federation/FederatedCalendarService.php index 9e76b1facb4..0173a986ba3 100644 --- a/apps/dav/lib/CalDAV/Federation/FederatedCalendarService.php +++ b/apps/dav/lib/CalDAV/Federation/FederatedCalendarService.php @@ -16,16 +16,16 @@ class FederatedCalendarService { } public function createFederatedCalendar( - FederatedCalendar $calendar, - string $principalUri, + FederatedCalendarImpl $calendar, + string $principalUri, ): void { $entity = $this->federatedCalendarToEntity($calendar, $principalUri); $this->federatedCalendarsMapper->insert($entity); } private function federatedCalendarToEntity( - FederatedCalendar $calendar, - string $principalUri, + FederatedCalendarImpl $calendar, + string $principalUri, ): FederatedCalendarEntity { $entity = new FederatedCalendarEntity(); $entity->setPrincipalUri($principalUri); diff --git a/apps/dav/lib/CalDAV/Federation/FederatedCalendar_actual.php b/apps/dav/lib/CalDAV/Federation/FederatedCalendar_actual.php deleted file mode 100644 index deeb70b871e..00000000000 --- a/apps/dav/lib/CalDAV/Federation/FederatedCalendar_actual.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace OCA\DAV\CalDAV\Federation; - -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CalDAV\Calendar; -use OCP\IConfig; -use OCP\IL10N; -use Psr\Log\LoggerInterface; -use Sabre\CalDAV\Backend; - -class FederatedCalendar_actual extends Calendar { - public function __construct( - Backend\BackendInterface $caldavBackend, - $calendarInfo, - IL10N $l10n, - IConfig $config, - LoggerInterface $logger, - private FederatedCalendarMapper $federatedCalendarMapper, - ) { - parent::__construct($caldavBackend, $calendarInfo, $l10n, $config, $logger); - } - - public function delete() { - $this->federatedCalendarMapper->deleteById($this->getResourceId()); - } - - protected function getCalendarType(): int { - return CalDavBackend::CALENDAR_TYPE_FEDERATED; - } - - /* - public function canWrite() { - // TODO: implement read-write sharing - return false; - } - */ -} diff --git a/apps/dav/lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php b/apps/dav/lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php index 360d00c32ec..c21b3c5585c 100644 --- a/apps/dav/lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php +++ b/apps/dav/lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php @@ -16,11 +16,13 @@ class CalendarFederationProtocolV1 implements ICalendarFederationProtocol { public const PROP_DISPLAY_NAME = 'displayName'; public const PROP_COLOR = 'color'; public const PROP_ACCESS = 'access'; + public const PROP_COMPONENTS = 'components'; private string $url = ''; private string $displayName = ''; private ?string $color = null; private int $access = 0; + private string $components = ''; /** * @throws CalendarProtocolParseException If parsing the raw protocol array fails. @@ -45,16 +47,23 @@ class CalendarFederationProtocolV1 implements ICalendarFederationProtocol { throw new CalendarProtocolParseException('Color is set but not a string'); } + // TODO: remove access until we have read-write support? $access = $rawProtocol[self::PROP_ACCESS] ?? null; if (!is_int($access)) { throw new CalendarProtocolParseException('Access is missing or not an integer'); } + $components = $rawProtocol[self::PROP_COMPONENTS] ?? null; + if (!is_string($components)) { + throw new CalendarProtocolParseException('Supported calendar components are missing or not a string'); + } + $protocol = new self(); $protocol->setUrl($url); $protocol->setDisplayName($displayName); $protocol->setColor($color); $protocol->setAccess($access); + $protocol->setComponents($components); return $protocol; } @@ -66,6 +75,7 @@ class CalendarFederationProtocolV1 implements ICalendarFederationProtocol { self::PROP_DISPLAY_NAME => $this->getDisplayName(), self::PROP_COLOR => $this->getColor(), self::PROP_ACCESS => $this->getAccess(), + self::PROP_COMPONENTS => $this->getComponents(), ]; } @@ -105,5 +115,13 @@ class CalendarFederationProtocolV1 implements ICalendarFederationProtocol { public function setAccess(int $access): void { $this->access = $access; } + + public function getComponents(): string { + return $this->components; + } + + public function setComponents(string $components): void { + $this->components = $components; + } } diff --git a/apps/dav/lib/CalDAV/Federation/RemoteUserCalendarHome.php b/apps/dav/lib/CalDAV/Federation/RemoteUserCalendarHome.php index 32aee7f9771..af65142b3c9 100644 --- a/apps/dav/lib/CalDAV/Federation/RemoteUserCalendarHome.php +++ b/apps/dav/lib/CalDAV/Federation/RemoteUserCalendarHome.php @@ -24,21 +24,28 @@ use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Node; class RemoteUserCalendarHome extends CalendarHome { - //private CalDavBackend $calDavBackend; private readonly IL10N $l10n; private readonly IConfig $config; private readonly LoggerInterface $logger; + private readonly FederatedCalendarMapper $federatedCalendarMapper; public function __construct(Backend\BackendInterface $caldavBackend, $principalInfo) { parent::__construct($caldavBackend, $principalInfo); - //$this->calDavBackend = $caldavBackend; + // TODO: inject? $this->l10n = Server::get(IL10NFactory::class)->get(Application::APP_ID); $this->config = Server::get(IConfig::class); $this->logger = Server::get(LoggerInterface::class); + $this->federatedCalendarMapper = Server::get(FederatedCalendarMapper::class); } public function getChild($name) { + // Usually, this should be optimized, but we can simply query all children here. + // Shares are selected by the sharee's principal, so there will be exactly one row for each + // calendar from this instance which has been shared with that particular remote user. + // Unless many calendars from this instance are shared with a single remote user, this + // should never be a performance concern. + /** @var Node[] $children */ $children = $this->getChildren(); foreach ($children as $child) { @@ -47,6 +54,7 @@ class RemoteUserCalendarHome extends CalendarHome { } } + throw new NotFound("Node with name $name could not be found"); } @@ -55,23 +63,27 @@ class RemoteUserCalendarHome extends CalendarHome { return []; } - $shares = $this->caldavBackend->getSharesByShareePrincipal($this->principalInfo['uri']); - $children = []; + + $shares = $this->caldavBackend->getSharesByShareePrincipal($this->principalInfo['uri']); foreach ($shares as $share) { + // Type Should always be "calendar" as the sharing service is scoped to calendars. + // Just to be sure ... if ($share['type'] !== 'calendar') { continue; } $calendar = $this->caldavBackend->getCalendarById($share['resourceid']); - $calendar['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'] = 1; // TODO: do not force read-only here + // TODO: implement read-write sharing + $calendar['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'] = 1; $calendar['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'] = $this->principalInfo['uri']; - $children[] = new Calendar( + $children[] = new FederatedCalendar( $this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger, + $this->federatedCalendarMapper, ); } diff --git a/apps/dav/lib/CalDAV/Federation/RemoteUserPrincipalParser.php b/apps/dav/lib/CalDAV/Federation/RemoteUserPrincipalParser.php new file mode 100644 index 00000000000..f0a9f8e0c49 --- /dev/null +++ b/apps/dav/lib/CalDAV/Federation/RemoteUserPrincipalParser.php @@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV\Federation; + +class RemoteUserPrincipalParser { + + public function parseEncodedPrincipal() { + + } + +} diff --git a/apps/dav/lib/CalDAV/Principal/RemoteUserCollection.php b/apps/dav/lib/CalDAV/Principal/RemoteUserCollection.php index b8917262e3a..a6d1a3c294a 100644 --- a/apps/dav/lib/CalDAV/Principal/RemoteUserCollection.php +++ b/apps/dav/lib/CalDAV/Principal/RemoteUserCollection.php @@ -11,13 +11,10 @@ namespace OCA\DAV\CalDAV\Principal; use OCA\DAV\DAV\RemoteUserPrincipalBackend; use OCA\DAV\DAV\Sharing\SharingMapper; -use OCP\Server; use Sabre\DAV\Exception\NotFound; -//use Sabre\DAV\SimpleCollection; class RemoteUserCollection extends \Sabre\DAV\Collection { - private readonly SharingMapper $sharingMapper; - private bool $hasCachedAllChildren; + private bool $hasCachedAllChildren = false; /** @var RemoteUser[] */ private array $children = []; @@ -27,12 +24,8 @@ class RemoteUserCollection extends \Sabre\DAV\Collection { public function __construct( private readonly RemoteUserPrincipalBackend $principalBackend, + private readonly SharingMapper $sharingMapper, ) { - // TODO: inject - $this->sharingMapper = Server::get(SharingMapper::class); - - //$children = $this->loadChildren(); - //parent::__construct('remote-users', $children); } public function getName() { @@ -58,9 +51,7 @@ class RemoteUserCollection extends \Sabre\DAV\Collection { } private function loadChildren(): array { - // TODO: do we need the resource type here as an argument? $rows = $this->sharingMapper->getRemoteUserPrincipalUris('calendar'); - return array_map(fn (array $row) => $this->rowToRemoteUser($row['principaluri']), $rows); } @@ -68,8 +59,7 @@ class RemoteUserCollection extends \Sabre\DAV\Collection { if (isset($this->childrenByName[$name])) { $child = $this->childrenByName[$name]; if ($child === null) { - // TODO: add message - throw new NotFound(); + throw new NotFound("Principal not found: $name"); } return $child; @@ -82,14 +72,19 @@ class RemoteUserCollection extends \Sabre\DAV\Collection { } } + // TODO: check the following claim + // It makes sense to load and cache only a single principal here as there are two main usage + // patterns: Either all principals are loaded via getChildren() (when listing) or a single, + // specific one is requested by path. $principalUri = "principals/remote-users/$name"; - // TODO: do we need the resource type here as an argument? if ($this->sharingMapper->hasRemoteUserPrincipalUri('calendar', $principalUri)) { $remoteUser = $this->rowToRemoteUser($principalUri); $this->childrenByName[$name] = $remoteUser; return $remoteUser; } + // Skip next search + $this->childrenByName[$name] = null; throw new NotFound(); } diff --git a/apps/dav/lib/CalDAV/Sharing/Backend.php b/apps/dav/lib/CalDAV/Sharing/Backend.php index 2403cafa18b..71bbbf237f3 100644 --- a/apps/dav/lib/CalDAV/Sharing/Backend.php +++ b/apps/dav/lib/CalDAV/Sharing/Backend.php @@ -31,4 +31,8 @@ class Backend extends SharingBackend { ) { parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->remoteUserPrincipalBackend, $this->cacheFactory, $this->service, $this->federationSharingService, $this->logger); } + + public function getShareByCalendarName(string $principal, string $calendarName): array { + return $this->service->getShareByCalendarName($principal, $calendarName); + } } diff --git a/apps/dav/lib/CalDAV/Sharing/Service.php b/apps/dav/lib/CalDAV/Sharing/Service.php index 4f0554f09bd..67315b1673f 100644 --- a/apps/dav/lib/CalDAV/Sharing/Service.php +++ b/apps/dav/lib/CalDAV/Sharing/Service.php @@ -18,4 +18,7 @@ class Service extends SharingService { ) { parent::__construct($mapper); } + + public function getShareByCalendarName(string $principal, string $calendarName): array { + } } diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php index 033b5f8d347..afcae136822 100644 --- a/apps/dav/lib/Command/CreateCalendar.php +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -9,6 +9,7 @@ namespace OCA\DAV\Command; use OC\KnownUser\KnownUserService; use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\Connector\Sabre\Principal; @@ -80,6 +81,7 @@ class CreateCalendar extends Command { $dispatcher, $config, Server::get(Backend::class), + Server::get(FederatedCalendarMapper::class), ); $caldav->createCalendar("principals/users/$user", $name, []); return self::SUCCESS; diff --git a/apps/dav/lib/DAV/RemoteUserPrincipalBackend.php b/apps/dav/lib/DAV/RemoteUserPrincipalBackend.php index 16053062107..0be289d343e 100644 --- a/apps/dav/lib/DAV/RemoteUserPrincipalBackend.php +++ b/apps/dav/lib/DAV/RemoteUserPrincipalBackend.php @@ -9,44 +9,62 @@ declare(strict_types=1); namespace OCA\DAV\DAV; +use OCA\DAV\DAV\Sharing\SharingMapper; use OCP\Federation\ICloudIdManager; -use OCP\Server; use Sabre\DAVACL\PrincipalBackend\BackendInterface; class RemoteUserPrincipalBackend implements BackendInterface { public const PRINCIPAL_PREFIX = 'principals/remote-users'; - private readonly ICloudIdManager $cloudIdManager; + private bool $hasCachedAllChildren = false; - public function __construct() { - // TODO: inject - $this->cloudIdManager = Server::get(ICloudIdManager::class); + /** @var array<string, mixed>[] */ + private array $principals = []; + + /** @var array<string, array<string, mixed>|null> */ + private array $principalsByPath = []; + + public function __construct( + private readonly ICloudIdManager $cloudIdManager, + private readonly SharingMapper $sharingMapper, + ) { } public function getPrincipalsByPrefix($prefixPath) { - // We don't know about all the remote principals on all remote instances - return []; + if ($prefixPath !== self::PRINCIPAL_PREFIX) { + return []; + } + + if (!$this->hasCachedAllChildren) { + $this->loadChildren(); + $this->hasCachedAllChildren = true; + } + + return $this->principals; } public function getPrincipalByPath($path) { - if (!str_starts_with($path, self::PRINCIPAL_PREFIX . '/')) { + [$prefix] = \Sabre\Uri\split($path); + if ($prefix !== self::PRINCIPAL_PREFIX) { return null; } - [,, $encodedPrincipal] = explode('/', $path); - [$encodedCloudId, $scope] = explode('|', base64_decode($encodedPrincipal)); - try { - $cloudId = $this->cloudIdManager->resolveCloudId($encodedCloudId); - } catch (\InvalidArgumentException $e) { + if (isset($this->principalsByPath[$path])) { + return $this->principalsByPath[$path]; + } + + // TODO: check the following claim + // It makes sense to load and cache only a single principal here as there are two main usage + // patterns: Either all principals are loaded via getChildren() (when listing) or a single, + // specific one is requested by path. + if (!$this->sharingMapper->hasShareWithPrincipalUri('calendar', $path)) { + $this->principalsByPath[$path] = null; return null; } - return [ - 'uri' => $path, - '{DAV:}displayname' => $cloudId->getDisplayId(), - '{http://nextcloud.com/ns}scope' => $scope, - //'{http://nextcloud.com/ns}federated-by' => $cloudId->getId(), - ]; + $principal = $this->principalUriToPrincipal($path); + $this->principalsByPath[$path] = $principal; + return $principal; } public function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) { @@ -84,6 +102,42 @@ class RemoteUserPrincipalBackend implements BackendInterface { } public function setGroupMemberSet($principal, array $members) { - throw new \Sabre\DAV\Exception('Setting members of the group is not supported yet'); + throw new \Sabre\DAV\Exception('Adding members to remote user is not supported'); + } + + /** + * @psalm-return list<string, string> [$cloudId, $scope] + */ + private function decodeRemoteUserPrincipalName(string $name): array { + [$cloudId, $scope] = explode('|', base64_decode($name)); + return [$cloudId, $scope]; + } + + /** + * @return array<string, array> + */ + private function principalUriToPrincipal(string $principalUri): array { + [, $name] = \Sabre\Uri\split($principalUri); + [$encodedCloudId, $scope] = $this->decodeRemoteUserPrincipalName($name); + $cloudId = $this->cloudIdManager->resolveCloudId($encodedCloudId); + return [ + 'uri' => $principalUri, + '{DAV:}displayname' => $cloudId->getDisplayId(), + '{http://nextcloud.com/ns}cloud-id' => $cloudId, + '{http://nextcloud.com/ns}federation-scope' => $scope, + ]; + } + + private function loadChildren(): array { + $rows = $this->sharingMapper->getPrincipalUrisByPrefix('calendar', self::PRINCIPAL_PREFIX); + $this->principals = array_map( + fn (array $row) => $this->principalUriToPrincipal($row['principaluri']), + $rows, + ); + + $this->principalsByPath = []; + foreach ($this->principals as $child) { + $this->principalsByPath[$child['uri']] = $child; + } } } diff --git a/apps/dav/lib/DAV/Sharing/SharingMapper.php b/apps/dav/lib/DAV/Sharing/SharingMapper.php index e3da42e0123..64767af5421 100644 --- a/apps/dav/lib/DAV/Sharing/SharingMapper.php +++ b/apps/dav/lib/DAV/Sharing/SharingMapper.php @@ -7,6 +7,7 @@ declare(strict_types=1); */ namespace OCA\DAV\DAV\Sharing; +use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -148,12 +149,23 @@ class SharingMapper { ->executeStatement(); } - public function hasRemoteUserPrincipalUri(string $resourceType, string $principalUri): bool { + /** + * @throws \OCP\DB\Exception + */ + public function hasShareWithPrincipalUri(string $resourceType, string $principalUri): bool { $query = $this->db->getQueryBuilder(); $result = $query->selectDistinct($query->func()->count('*')) ->from('dav_shares') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)) - ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)) + ->where($query->expr()->eq( + 'principaluri', + $query->createNamedParameter($principalUri, IQueryBuilder::PARAM_STR), + IQueryBuilder::PARAM_STR, + )) + ->andWhere($query->expr()->eq( + 'type', + $query->createNamedParameter($resourceType, IQueryBuilder::PARAM_STR), + IQueryBuilder::PARAM_STR, + )) ->executeQuery(); $count = (int)$result->fetchOne(); @@ -162,12 +174,24 @@ class SharingMapper { return $count > 0; } - public function getRemoteUserPrincipalUris(string $resourceType): array { + /** + * @return array{principaluri: string}[] + * @throws \OCP\DB\Exception + */ + public function getPrincipalUrisByPrefix(string $resourceType, string $prefix): array { $query = $this->db->getQueryBuilder(); $result = $query->selectDistinct('principaluri') ->from('dav_shares') - ->where($query->expr()->like('principaluri', $query->createNamedParameter('principals/remote-users/%'))) - ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType))) + ->where($query->expr()->like( + 'principaluri', + $query->createNamedParameter("$prefix/%", IQueryBuilder::PARAM_STR), + IQueryBuilder::PARAM_STR, + )) + ->andWhere($query->expr()->eq( + 'type', + $query->createNamedParameter($resourceType, IQueryBuilder::PARAM_STR)), + IQueryBuilder::PARAM_STR, + ) ->executeQuery(); $rows = $result->fetchAll(); diff --git a/apps/dav/lib/Migration/Version1034Date20250605132605.php b/apps/dav/lib/Migration/Version1034Date20250605132605.php index 3267ea80be0..57e478a6ac3 100644 --- a/apps/dav/lib/Migration/Version1034Date20250605132605.php +++ b/apps/dav/lib/Migration/Version1034Date20250605132605.php @@ -84,6 +84,10 @@ class Version1034Date20250605132605 extends SimpleMigrationStep { 'notnull' => true, 'length' => 255, ]); + $federatedCalendarsTable->addColumn('components', Types::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); $federatedCalendarsTable->setPrimaryKey(['id']); } diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index 4fc682ceace..07d5649b015 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -11,8 +11,8 @@ use OC\KnownUser\KnownUserService; use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalendarRoot; +use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper; use OCA\DAV\CalDAV\Principal\Collection; -use OCA\DAV\CalDAV\Principal\RemoteUserCollection; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\PublicCalendarRoot; use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend; @@ -78,7 +78,7 @@ class RootCollection extends SimpleCollection { $groupPrincipalBackend = new GroupPrincipalBackend($groupManager, $userSession, $shareManager, $config); $calendarResourcePrincipalBackend = new ResourcePrincipalBackend($db, $userSession, $groupManager, $logger, $proxyMapper); $calendarRoomPrincipalBackend = new RoomPrincipalBackend($db, $userSession, $groupManager, $logger, $proxyMapper); - $remoteUserPrincipalBackend = new RemoteUserPrincipalBackend(); + $remoteUserPrincipalBackend = Server::get(RemoteUserPrincipalBackend::class); // as soon as debug mode is enabled we allow listing of principals $disableListing = !$config->getSystemValue('debug', false); @@ -105,6 +105,7 @@ class RootCollection extends SimpleCollection { $dispatcher, $config, $calendarSharingBackend, + Server::get(FederatedCalendarMapper::class), false, ); $userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users', $logger); diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php index c1d8e8609b6..40b8b455dd4 100644 --- a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php +++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php @@ -9,10 +9,13 @@ namespace OCA\DAV\Tests\unit\CalDAV; use OC\KnownUser\KnownUserService; use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper; +use OCA\DAV\CalDAV\Federation\FederationSharingService; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Sharing\Backend as SharingBackend; use OCA\DAV\CalDAV\Sharing\Service; use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\RemoteUserPrincipalBackend; use OCA\DAV\DAV\Sharing\SharingMapper; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; @@ -53,6 +56,9 @@ abstract class AbstractCalDavBackend extends TestCase { private ISecureRandom $random; protected SharingBackend $sharingBackend; protected IDBConnection $db; + protected RemoteUserPrincipalBackend&MockObject $remoteUserPrincipalBackend; + protected FederationSharingService&MockObject $federationSharingService; + protected FederatedCalendarMapper&MockObject $federatedCalendarMapper; public const UNIT_TEST_USER = 'principals/users/caldav-unit-test'; public const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1'; public const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group'; @@ -92,12 +98,17 @@ abstract class AbstractCalDavBackend extends TestCase { $this->random = Server::get(ISecureRandom::class); $this->logger = $this->createMock(LoggerInterface::class); $this->config = $this->createMock(IConfig::class); + $this->remoteUserPrincipalBackend = $this->createMock(RemoteUserPrincipalBackend::class); + $this->federationSharingService = $this->createMock(FederationSharingService::class); + $this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class); $this->sharingBackend = new SharingBackend( $this->userManager, $this->groupManager, $this->principal, + $this->remoteUserPrincipalBackend, $this->createMock(ICacheFactory::class), new Service(new SharingMapper($this->db)), + $this->federationSharingService, $this->logger); $this->backend = new CalDavBackend( $this->db, @@ -108,6 +119,7 @@ abstract class AbstractCalDavBackend extends TestCase { $this->dispatcher, $this->config, $this->sharingBackend, + $this->federatedCalendarMapper, false, ); diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php index 075681eff7f..4a640208d4a 100644 --- a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php +++ b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php @@ -7,6 +7,7 @@ namespace OCA\DAV\Tests\unit\CalDAV; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper; use OCA\DAV\CalDAV\PublicCalendar; use OCA\DAV\CalDAV\PublicCalendarRoot; use OCA\DAV\Connector\Sabre\Principal; @@ -18,6 +19,7 @@ use OCP\IL10N; use OCP\IUserManager; use OCP\Security\ISecureRandom; use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Test\TestCase; @@ -50,6 +52,8 @@ class PublicCalendarRootTest extends TestCase { /** @var LoggerInterface */ private $logger; + protected FederatedCalendarMapper&MockObject $federatedCalendarMapper; + protected function setUp(): void { parent::setUp(); @@ -59,6 +63,7 @@ class PublicCalendarRootTest extends TestCase { $this->groupManager = $this->createMock(IGroupManager::class); $this->random = Server::get(ISecureRandom::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class); $dispatcher = $this->createMock(IEventDispatcher::class); $config = $this->createMock(IConfig::class); $sharingBackend = $this->createMock(\OCA\DAV\CalDAV\Sharing\Backend::class); @@ -80,6 +85,7 @@ class PublicCalendarRootTest extends TestCase { $dispatcher, $config, $sharingBackend, + $this->federatedCalendarMapper, false, ); $this->l10n = $this->getMockBuilder(IL10N::class) diff --git a/apps/federation/lib/Command/SyncFederationCalendars.php b/apps/federation/lib/Command/SyncFederationCalendars.php index bca430068a8..4dc2ea7ba58 100644 --- a/apps/federation/lib/Command/SyncFederationCalendars.php +++ b/apps/federation/lib/Command/SyncFederationCalendars.php @@ -30,6 +30,10 @@ class SyncFederationCalendars extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $calendarCount = $this->federatedCalendarMapper->countAll(); + if ($calendarCount === 0) { + $output->writeln('There are no federated calendars'); + return 0; + } $progress = new ProgressBar($output, $calendarCount); $progress->start(); |