aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Steinmetz <richard@steinmetz.cloud>2025-06-13 14:45:12 +0200
committerRichard Steinmetz <richard@steinmetz.cloud>2025-06-13 14:45:12 +0200
commit4fd1ccc62741c9a7f4fd005713a96894b80378cd (patch)
tree99d37a1c8dc11b1aa9900c42c11ceb576fd47f9b
parent2a21c74ab201d08d9fbd396d6e7becb71e0273e6 (diff)
downloadnextcloud-server-feat/federated-calendar-sharing.tar.gz
nextcloud-server-feat/federated-calendar-sharing.zip
fixup! feat: calendar federationfeat/federated-calendar-sharing
-rw-r--r--apps/dav/appinfo/v1/caldav.php2
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php3
-rw-r--r--apps/dav/composer/composer/autoload_static.php3
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php20
-rw-r--r--apps/dav/lib/CalDAV/CalendarHome.php81
-rw-r--r--apps/dav/lib/CalDAV/CalendarProvider.php28
-rw-r--r--apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php34
-rw-r--r--apps/dav/lib/CalDAV/Federation/FederatedCalendar.php87
-rw-r--r--apps/dav/lib/CalDAV/Federation/FederatedCalendarEntity.php38
-rw-r--r--apps/dav/lib/CalDAV/Federation/FederatedCalendarImpl.php79
-rw-r--r--apps/dav/lib/CalDAV/Federation/FederatedCalendarMapper.php3
-rw-r--r--apps/dav/lib/CalDAV/Federation/FederatedCalendarProvider.php2
-rw-r--r--apps/dav/lib/CalDAV/Federation/FederatedCalendarService.php8
-rw-r--r--apps/dav/lib/CalDAV/Federation/FederatedCalendar_actual.php45
-rw-r--r--apps/dav/lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php18
-rw-r--r--apps/dav/lib/CalDAV/Federation/RemoteUserCalendarHome.php24
-rw-r--r--apps/dav/lib/CalDAV/Federation/RemoteUserPrincipalParser.php18
-rw-r--r--apps/dav/lib/CalDAV/Principal/RemoteUserCollection.php23
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Backend.php4
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Service.php3
-rw-r--r--apps/dav/lib/Command/CreateCalendar.php2
-rw-r--r--apps/dav/lib/DAV/RemoteUserPrincipalBackend.php94
-rw-r--r--apps/dav/lib/DAV/Sharing/SharingMapper.php36
-rw-r--r--apps/dav/lib/Migration/Version1034Date20250605132605.php4
-rw-r--r--apps/dav/lib/RootCollection.php5
-rw-r--r--apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php12
-rw-r--r--apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php6
-rw-r--r--apps/federation/lib/Command/SyncFederationCalendars.php4
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();