mirror of
https://github.com/nextcloud/server.git
synced 2026-02-20 00:12:30 -05:00
feat: calendar federation
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
This commit is contained in:
parent
45f5daa45a
commit
b7dc720848
66 changed files with 4343 additions and 189 deletions
|
|
@ -10,7 +10,7 @@
|
|||
<name>WebDAV</name>
|
||||
<summary>WebDAV endpoint</summary>
|
||||
<description>WebDAV endpoint</description>
|
||||
<version>1.34.1</version>
|
||||
<version>1.34.2</version>
|
||||
<licence>agpl</licence>
|
||||
<author>owncloud.org</author>
|
||||
<namespace>DAV</namespace>
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
<job>OCA\DAV\BackgroundJob\EventReminderJob</job>
|
||||
<job>OCA\DAV\BackgroundJob\CalendarRetentionJob</job>
|
||||
<job>OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob</job>
|
||||
<job>OCA\DAV\BackgroundJob\FederatedCalendarPeriodicSyncJob</job>
|
||||
</background-jobs>
|
||||
|
||||
<repair-steps>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use OC\KnownUser\KnownUserService;
|
|||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\CalendarRoot;
|
||||
use OCA\DAV\CalDAV\DefaultCalendarValidator;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarFactory;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
|
||||
use OCA\DAV\CalDAV\Schedule\IMipPlugin;
|
||||
use OCA\DAV\CalDAV\Security\RateLimitingPlugin;
|
||||
|
|
@ -29,6 +31,7 @@ use OCP\IRequest;
|
|||
use OCP\ISession;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IUserSession;
|
||||
use OCP\L10N\IFactory as IL10NFactory;
|
||||
use OCP\Security\Bruteforce\IThrottler;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use OCP\Server;
|
||||
|
|
@ -61,6 +64,9 @@ $random = Server::get(ISecureRandom::class);
|
|||
$logger = Server::get(LoggerInterface::class);
|
||||
$dispatcher = Server::get(IEventDispatcher::class);
|
||||
$config = Server::get(IConfig::class);
|
||||
$l10nFactory = Server::get(IL10NFactory::class);
|
||||
$davL10n = $l10nFactory->get('dav');
|
||||
$federatedCalendarFactory = Server::get(FederatedCalendarFactory::class);
|
||||
|
||||
$calDavBackend = new CalDavBackend(
|
||||
$db,
|
||||
|
|
@ -71,6 +77,7 @@ $calDavBackend = new CalDavBackend(
|
|||
$dispatcher,
|
||||
$config,
|
||||
Server::get(\OCA\DAV\CalDAV\Sharing\Backend::class),
|
||||
Server::get(FederatedCalendarMapper::class),
|
||||
true
|
||||
);
|
||||
|
||||
|
|
@ -81,7 +88,7 @@ $sendInvitations = Server::get(IConfig::class)->getAppValue('dav', 'sendInvitati
|
|||
$principalCollection = new \Sabre\CalDAV\Principal\Collection($principalBackend);
|
||||
$principalCollection->disableListing = !$debugging; // Disable listing
|
||||
|
||||
$addressBookRoot = new CalendarRoot($principalBackend, $calDavBackend, 'principals', $logger);
|
||||
$addressBookRoot = new CalendarRoot($principalBackend, $calDavBackend, 'principals', $logger, $davL10n, $config, $federatedCalendarFactory);
|
||||
$addressBookRoot->disableListing = !$debugging; // Disable listing
|
||||
|
||||
$nodes = [
|
||||
|
|
@ -96,7 +103,7 @@ $server->httpRequest->setUrl(Server::get(IRequest::class)->getRequestUri());
|
|||
$server->setBaseUri($baseuri);
|
||||
|
||||
// Add plugins
|
||||
$server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), \OC::$server->getL10N('dav')));
|
||||
$server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), $davL10n));
|
||||
$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend));
|
||||
$server->addPlugin(new \Sabre\CalDAV\Plugin());
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ return array(
|
|||
'OCA\\DAV\\BackgroundJob\\CleanupOrphanedChildrenJob' => $baseDir . '/../lib/BackgroundJob/CleanupOrphanedChildrenJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\DeleteOutdatedSchedulingObjects' => $baseDir . '/../lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php',
|
||||
'OCA\\DAV\\BackgroundJob\\EventReminderJob' => $baseDir . '/../lib/BackgroundJob/EventReminderJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\FederatedCalendarPeriodicSyncJob' => $baseDir . '/../lib/BackgroundJob/FederatedCalendarPeriodicSyncJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\FederatedCalendarSyncJob' => $baseDir . '/../lib/BackgroundJob/FederatedCalendarSyncJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => $baseDir . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => $baseDir . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php',
|
||||
|
|
@ -66,6 +68,21 @@ return array(
|
|||
'OCA\\DAV\\CalDAV\\EventReaderRDate' => $baseDir . '/../lib/CalDAV/EventReaderRDate.php',
|
||||
'OCA\\DAV\\CalDAV\\EventReaderRRule' => $baseDir . '/../lib/CalDAV/EventReaderRRule.php',
|
||||
'OCA\\DAV\\CalDAV\\Export\\ExportService' => $baseDir . '/../lib/CalDAV/Export/ExportService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\CalendarFederationConfig' => $baseDir . '/../lib/CalDAV/Federation/CalendarFederationConfig.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\CalendarFederationNotifier' => $baseDir . '/../lib/CalDAV/Federation/CalendarFederationNotifier.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\CalendarFederationProvider' => $baseDir . '/../lib/CalDAV/Federation/CalendarFederationProvider.php',
|
||||
'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\\FederatedCalendarFactory' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarFactory.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\\FederatedCalendarSyncService' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => $baseDir . '/../lib/CalDAV/Federation/FederationSharingService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarFederationProtocolV1' => $baseDir . '/../lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php',
|
||||
'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\\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',
|
||||
|
|
@ -116,6 +133,8 @@ return array(
|
|||
'OCA\\DAV\\CalDAV\\Sharing\\Backend' => $baseDir . '/../lib/CalDAV/Sharing/Backend.php',
|
||||
'OCA\\DAV\\CalDAV\\Sharing\\Service' => $baseDir . '/../lib/CalDAV/Sharing/Service.php',
|
||||
'OCA\\DAV\\CalDAV\\Status\\StatusService' => $baseDir . '/../lib/CalDAV/Status/StatusService.php',
|
||||
'OCA\\DAV\\CalDAV\\SyncService' => $baseDir . '/../lib/CalDAV/SyncService.php',
|
||||
'OCA\\DAV\\CalDAV\\SyncServiceResult' => $baseDir . '/../lib/CalDAV/SyncServiceResult.php',
|
||||
'OCA\\DAV\\CalDAV\\TimeZoneFactory' => $baseDir . '/../lib/CalDAV/TimeZoneFactory.php',
|
||||
'OCA\\DAV\\CalDAV\\TimezoneService' => $baseDir . '/../lib/CalDAV/TimezoneService.php',
|
||||
'OCA\\DAV\\CalDAV\\TipBroker' => $baseDir . '/../lib/CalDAV/TipBroker.php',
|
||||
|
|
@ -243,6 +262,7 @@ return array(
|
|||
'OCA\\DAV\\DAV\\CustomPropertiesBackend' => $baseDir . '/../lib/DAV/CustomPropertiesBackend.php',
|
||||
'OCA\\DAV\\DAV\\GroupPrincipalBackend' => $baseDir . '/../lib/DAV/GroupPrincipalBackend.php',
|
||||
'OCA\\DAV\\DAV\\PublicAuth' => $baseDir . '/../lib/DAV/PublicAuth.php',
|
||||
'OCA\\DAV\\DAV\\RemoteUserPrincipalBackend' => $baseDir . '/../lib/DAV/RemoteUserPrincipalBackend.php',
|
||||
'OCA\\DAV\\DAV\\Sharing\\Backend' => $baseDir . '/../lib/DAV/Sharing/Backend.php',
|
||||
'OCA\\DAV\\DAV\\Sharing\\IShareable' => $baseDir . '/../lib/DAV/Sharing/IShareable.php',
|
||||
'OCA\\DAV\\DAV\\Sharing\\Plugin' => $baseDir . '/../lib/DAV/Sharing/Plugin.php',
|
||||
|
|
@ -304,6 +324,7 @@ return array(
|
|||
'OCA\\DAV\\Listener\\BirthdayListener' => $baseDir . '/../lib/Listener/BirthdayListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarContactInteractionListener' => $baseDir . '/../lib/Listener/CalendarContactInteractionListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarDeletionDefaultUpdaterListener' => $baseDir . '/../lib/Listener/CalendarDeletionDefaultUpdaterListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarFederationNotificationListener' => $baseDir . '/../lib/Listener/CalendarFederationNotificationListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarObjectReminderUpdaterListener' => $baseDir . '/../lib/Listener/CalendarObjectReminderUpdaterListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarPublicationListener' => $baseDir . '/../lib/Listener/CalendarPublicationListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarShareUpdateListener' => $baseDir . '/../lib/Listener/CalendarShareUpdateListener.php',
|
||||
|
|
@ -311,6 +332,7 @@ return array(
|
|||
'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => $baseDir . '/../lib/Listener/ClearPhotoCacheListener.php',
|
||||
'OCA\\DAV\\Listener\\DavAdminSettingsListener' => $baseDir . '/../lib/Listener/DavAdminSettingsListener.php',
|
||||
'OCA\\DAV\\Listener\\OutOfOfficeListener' => $baseDir . '/../lib/Listener/OutOfOfficeListener.php',
|
||||
'OCA\\DAV\\Listener\\SabrePluginAuthInitListener' => $baseDir . '/../lib/Listener/SabrePluginAuthInitListener.php',
|
||||
'OCA\\DAV\\Listener\\SubscriptionListener' => $baseDir . '/../lib/Listener/SubscriptionListener.php',
|
||||
'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => $baseDir . '/../lib/Listener/TrustedServerRemovedListener.php',
|
||||
'OCA\\DAV\\Listener\\UserEventsListener' => $baseDir . '/../lib/Listener/UserEventsListener.php',
|
||||
|
|
@ -359,6 +381,7 @@ return array(
|
|||
'OCA\\DAV\\Migration\\Version1029Date20231004091403' => $baseDir . '/../lib/Migration/Version1029Date20231004091403.php',
|
||||
'OCA\\DAV\\Migration\\Version1030Date20240205103243' => $baseDir . '/../lib/Migration/Version1030Date20240205103243.php',
|
||||
'OCA\\DAV\\Migration\\Version1031Date20240610134258' => $baseDir . '/../lib/Migration/Version1031Date20240610134258.php',
|
||||
'OCA\\DAV\\Migration\\Version1034Date20250605132605' => $baseDir . '/../lib/Migration/Version1034Date20250605132605.php',
|
||||
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => $baseDir . '/../lib/Migration/Version1034Date20250813093701.php',
|
||||
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
|
||||
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
|
||||
|
|
@ -375,6 +398,7 @@ return array(
|
|||
'OCA\\DAV\\Search\\TasksSearchProvider' => $baseDir . '/../lib/Search/TasksSearchProvider.php',
|
||||
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
|
||||
'OCA\\DAV\\ServerFactory' => $baseDir . '/../lib/ServerFactory.php',
|
||||
'OCA\\DAV\\Service\\ASyncService' => $baseDir . '/../lib/Service/ASyncService.php',
|
||||
'OCA\\DAV\\Service\\AbsenceService' => $baseDir . '/../lib/Service/AbsenceService.php',
|
||||
'OCA\\DAV\\Service\\ExampleContactService' => $baseDir . '/../lib/Service/ExampleContactService.php',
|
||||
'OCA\\DAV\\Service\\ExampleEventService' => $baseDir . '/../lib/Service/ExampleEventService.php',
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\BackgroundJob\\CleanupOrphanedChildrenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupOrphanedChildrenJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\DeleteOutdatedSchedulingObjects' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php',
|
||||
'OCA\\DAV\\BackgroundJob\\EventReminderJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/EventReminderJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\FederatedCalendarPeriodicSyncJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/FederatedCalendarPeriodicSyncJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\FederatedCalendarSyncJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/FederatedCalendarSyncJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php',
|
||||
|
|
@ -81,6 +83,21 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\CalDAV\\EventReaderRDate' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRDate.php',
|
||||
'OCA\\DAV\\CalDAV\\EventReaderRRule' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRRule.php',
|
||||
'OCA\\DAV\\CalDAV\\Export\\ExportService' => __DIR__ . '/..' . '/../lib/CalDAV/Export/ExportService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\CalendarFederationConfig' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/CalendarFederationConfig.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\CalendarFederationNotifier' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/CalendarFederationNotifier.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\CalendarFederationProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/CalendarFederationProvider.php',
|
||||
'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\\FederatedCalendarFactory' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarFactory.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\\FederatedCalendarSyncService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederationSharingService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarFederationProtocolV1' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php',
|
||||
'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\\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',
|
||||
|
|
@ -131,6 +148,8 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\CalDAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/CalDAV/Sharing/Backend.php',
|
||||
'OCA\\DAV\\CalDAV\\Sharing\\Service' => __DIR__ . '/..' . '/../lib/CalDAV/Sharing/Service.php',
|
||||
'OCA\\DAV\\CalDAV\\Status\\StatusService' => __DIR__ . '/..' . '/../lib/CalDAV/Status/StatusService.php',
|
||||
'OCA\\DAV\\CalDAV\\SyncService' => __DIR__ . '/..' . '/../lib/CalDAV/SyncService.php',
|
||||
'OCA\\DAV\\CalDAV\\SyncServiceResult' => __DIR__ . '/..' . '/../lib/CalDAV/SyncServiceResult.php',
|
||||
'OCA\\DAV\\CalDAV\\TimeZoneFactory' => __DIR__ . '/..' . '/../lib/CalDAV/TimeZoneFactory.php',
|
||||
'OCA\\DAV\\CalDAV\\TimezoneService' => __DIR__ . '/..' . '/../lib/CalDAV/TimezoneService.php',
|
||||
'OCA\\DAV\\CalDAV\\TipBroker' => __DIR__ . '/..' . '/../lib/CalDAV/TipBroker.php',
|
||||
|
|
@ -258,6 +277,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\DAV\\CustomPropertiesBackend' => __DIR__ . '/..' . '/../lib/DAV/CustomPropertiesBackend.php',
|
||||
'OCA\\DAV\\DAV\\GroupPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/GroupPrincipalBackend.php',
|
||||
'OCA\\DAV\\DAV\\PublicAuth' => __DIR__ . '/..' . '/../lib/DAV/PublicAuth.php',
|
||||
'OCA\\DAV\\DAV\\RemoteUserPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/RemoteUserPrincipalBackend.php',
|
||||
'OCA\\DAV\\DAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Backend.php',
|
||||
'OCA\\DAV\\DAV\\Sharing\\IShareable' => __DIR__ . '/..' . '/../lib/DAV/Sharing/IShareable.php',
|
||||
'OCA\\DAV\\DAV\\Sharing\\Plugin' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Plugin.php',
|
||||
|
|
@ -319,6 +339,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\Listener\\BirthdayListener' => __DIR__ . '/..' . '/../lib/Listener/BirthdayListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarContactInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarContactInteractionListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarDeletionDefaultUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarDeletionDefaultUpdaterListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarFederationNotificationListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarFederationNotificationListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarObjectReminderUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarObjectReminderUpdaterListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarPublicationListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarPublicationListener.php',
|
||||
'OCA\\DAV\\Listener\\CalendarShareUpdateListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarShareUpdateListener.php',
|
||||
|
|
@ -326,6 +347,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => __DIR__ . '/..' . '/../lib/Listener/ClearPhotoCacheListener.php',
|
||||
'OCA\\DAV\\Listener\\DavAdminSettingsListener' => __DIR__ . '/..' . '/../lib/Listener/DavAdminSettingsListener.php',
|
||||
'OCA\\DAV\\Listener\\OutOfOfficeListener' => __DIR__ . '/..' . '/../lib/Listener/OutOfOfficeListener.php',
|
||||
'OCA\\DAV\\Listener\\SabrePluginAuthInitListener' => __DIR__ . '/..' . '/../lib/Listener/SabrePluginAuthInitListener.php',
|
||||
'OCA\\DAV\\Listener\\SubscriptionListener' => __DIR__ . '/..' . '/../lib/Listener/SubscriptionListener.php',
|
||||
'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => __DIR__ . '/..' . '/../lib/Listener/TrustedServerRemovedListener.php',
|
||||
'OCA\\DAV\\Listener\\UserEventsListener' => __DIR__ . '/..' . '/../lib/Listener/UserEventsListener.php',
|
||||
|
|
@ -374,6 +396,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\Migration\\Version1029Date20231004091403' => __DIR__ . '/..' . '/../lib/Migration/Version1029Date20231004091403.php',
|
||||
'OCA\\DAV\\Migration\\Version1030Date20240205103243' => __DIR__ . '/..' . '/../lib/Migration/Version1030Date20240205103243.php',
|
||||
'OCA\\DAV\\Migration\\Version1031Date20240610134258' => __DIR__ . '/..' . '/../lib/Migration/Version1031Date20240610134258.php',
|
||||
'OCA\\DAV\\Migration\\Version1034Date20250605132605' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250605132605.php',
|
||||
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250813093701.php',
|
||||
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
|
||||
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
|
||||
|
|
@ -390,6 +413,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\Search\\TasksSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TasksSearchProvider.php',
|
||||
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
|
||||
'OCA\\DAV\\ServerFactory' => __DIR__ . '/..' . '/../lib/ServerFactory.php',
|
||||
'OCA\\DAV\\Service\\ASyncService' => __DIR__ . '/..' . '/../lib/Service/ASyncService.php',
|
||||
'OCA\\DAV\\Service\\AbsenceService' => __DIR__ . '/..' . '/../lib/Service/AbsenceService.php',
|
||||
'OCA\\DAV\\Service\\ExampleContactService' => __DIR__ . '/..' . '/../lib/Service/ExampleContactService.php',
|
||||
'OCA\\DAV\\Service\\ExampleEventService' => __DIR__ . '/..' . '/../lib/Service/ExampleEventService.php',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
|
|||
use OCA\DAV\CalDAV\CachedSubscriptionProvider;
|
||||
use OCA\DAV\CalDAV\CalendarManager;
|
||||
use OCA\DAV\CalDAV\CalendarProvider;
|
||||
use OCA\DAV\CalDAV\Federation\CalendarFederationProvider;
|
||||
use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
|
||||
use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
|
||||
use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
|
||||
|
|
@ -36,6 +37,7 @@ use OCA\DAV\Events\CalendarUpdatedEvent;
|
|||
use OCA\DAV\Events\CardCreatedEvent;
|
||||
use OCA\DAV\Events\CardDeletedEvent;
|
||||
use OCA\DAV\Events\CardUpdatedEvent;
|
||||
use OCA\DAV\Events\SabrePluginAuthInitEvent;
|
||||
use OCA\DAV\Events\SubscriptionCreatedEvent;
|
||||
use OCA\DAV\Events\SubscriptionDeletedEvent;
|
||||
use OCA\DAV\Listener\ActivityUpdaterListener;
|
||||
|
|
@ -44,6 +46,7 @@ use OCA\DAV\Listener\AddressbookListener;
|
|||
use OCA\DAV\Listener\BirthdayListener;
|
||||
use OCA\DAV\Listener\CalendarContactInteractionListener;
|
||||
use OCA\DAV\Listener\CalendarDeletionDefaultUpdaterListener;
|
||||
use OCA\DAV\Listener\CalendarFederationNotificationListener;
|
||||
use OCA\DAV\Listener\CalendarObjectReminderUpdaterListener;
|
||||
use OCA\DAV\Listener\CalendarPublicationListener;
|
||||
use OCA\DAV\Listener\CalendarShareUpdateListener;
|
||||
|
|
@ -51,6 +54,7 @@ use OCA\DAV\Listener\CardListener;
|
|||
use OCA\DAV\Listener\ClearPhotoCacheListener;
|
||||
use OCA\DAV\Listener\DavAdminSettingsListener;
|
||||
use OCA\DAV\Listener\OutOfOfficeListener;
|
||||
use OCA\DAV\Listener\SabrePluginAuthInitListener;
|
||||
use OCA\DAV\Listener\SubscriptionListener;
|
||||
use OCA\DAV\Listener\TrustedServerRemovedListener;
|
||||
use OCA\DAV\Listener\UserEventsListener;
|
||||
|
|
@ -82,6 +86,7 @@ use OCP\Config\BeforePreferenceSetEvent;
|
|||
use OCP\Contacts\IManager as IContactsManager;
|
||||
use OCP\DB\Events\AddMissingIndicesEvent;
|
||||
use OCP\Federation\Events\TrustedServerRemovedEvent;
|
||||
use OCP\Federation\ICloudFederationProviderManager;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Server;
|
||||
use OCP\Settings\Events\DeclarativeSettingsGetValueEvent;
|
||||
|
|
@ -198,6 +203,12 @@ class Application extends App implements IBootstrap {
|
|||
$context->registerEventListener(UserChangedEvent::class, UserEventsListener::class);
|
||||
$context->registerEventListener(UserUpdatedEvent::class, UserEventsListener::class);
|
||||
|
||||
$context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class);
|
||||
|
||||
$context->registerEventListener(CalendarObjectCreatedEvent::class, CalendarFederationNotificationListener::class);
|
||||
$context->registerEventListener(CalendarObjectUpdatedEvent::class, CalendarFederationNotificationListener::class);
|
||||
$context->registerEventListener(CalendarObjectDeletedEvent::class, CalendarFederationNotificationListener::class);
|
||||
|
||||
$context->registerNotifierService(Notifier::class);
|
||||
|
||||
$context->registerCalendarProvider(CalendarProvider::class);
|
||||
|
|
@ -213,7 +224,6 @@ class Application extends App implements IBootstrap {
|
|||
$context->registerDeclarativeSettings(SystemAddressBookSettings::class);
|
||||
$context->registerEventListener(DeclarativeSettingsGetValueEvent::class, DavAdminSettingsListener::class);
|
||||
$context->registerEventListener(DeclarativeSettingsSetValueEvent::class, DavAdminSettingsListener::class);
|
||||
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
|
|
@ -223,6 +233,7 @@ class Application extends App implements IBootstrap {
|
|||
$context->injectFn($this->registerContactsManager(...));
|
||||
$context->injectFn($this->registerCalendarManager(...));
|
||||
$context->injectFn($this->registerCalendarReminders(...));
|
||||
$context->injectFn($this->registerCloudFederationProvider(...));
|
||||
}
|
||||
|
||||
public function registerContactsManager(IContactsManager $cm, IAppContainer $container): void {
|
||||
|
|
@ -279,4 +290,14 @@ class Application extends App implements IBootstrap {
|
|||
$logger->error($ex->getMessage(), ['exception' => $ex]);
|
||||
}
|
||||
}
|
||||
|
||||
public function registerCloudFederationProvider(
|
||||
ICloudFederationProviderManager $manager,
|
||||
): void {
|
||||
$manager->addCloudFederationProvider(
|
||||
CalendarFederationProvider::PROVIDER_ID,
|
||||
'Calendar Federation',
|
||||
static fn () => Server::get(CalendarFederationProvider::class),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\BackgroundJob;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\CalendarFederationConfig;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\TimedJob;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FederatedCalendarPeriodicSyncJob extends TimedJob {
|
||||
private const DOWNLOAD_LIMIT = 500;
|
||||
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
private readonly FederatedCalendarSyncService $syncService,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly CalendarFederationConfig $calendarFederationConfig,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct($time);
|
||||
|
||||
$this->setTimeSensitivity(self::TIME_SENSITIVE);
|
||||
$this->setAllowParallelRuns(false);
|
||||
$this->setInterval(3600);
|
||||
}
|
||||
|
||||
protected function run($argument): void {
|
||||
if (!$this->calendarFederationConfig->isFederationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$downloadedEvents = 0;
|
||||
$oneHourAgo = $this->time->getTime() - 3600;
|
||||
$calendars = $this->federatedCalendarMapper->findUnsyncedSinceBefore($oneHourAgo);
|
||||
foreach ($calendars as $calendar) {
|
||||
try {
|
||||
$downloadedEvents += $this->syncService->syncOne($calendar);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$name = $calendar->getUri();
|
||||
$this->logger->error("Failed to sync federated calendar $name: " . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'calendar' => $calendar->toCalendarInfo(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Prevent stalling the background job queue for too long
|
||||
if ($downloadedEvents >= self::DOWNLOAD_LIMIT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
apps/dav/lib/BackgroundJob/FederatedCalendarSyncJob.php
Normal file
67
apps/dav/lib/BackgroundJob/FederatedCalendarSyncJob.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\BackgroundJob;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\CalendarFederationConfig;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\QueuedJob;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FederatedCalendarSyncJob extends QueuedJob {
|
||||
public const ARGUMENT_ID = 'id';
|
||||
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
private readonly FederatedCalendarSyncService $syncService,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly CalendarFederationConfig $calendarFederationConfig,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct($time);
|
||||
|
||||
$this->setAllowParallelRuns(false);
|
||||
}
|
||||
|
||||
protected function run($argument): void {
|
||||
if (!$this->calendarFederationConfig->isFederationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $argument[self::ARGUMENT_ID] ?? null;
|
||||
if (!is_numeric($id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = (int)$id;
|
||||
try {
|
||||
$calendar = $this->federatedCalendarMapper->find($id);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->syncService->syncOne($calendar);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$name = $calendar->getUri();
|
||||
$this->logger->error("Failed to sync federated calendar $name: " . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'calendar' => $calendar->toCalendarInfo(),
|
||||
]);
|
||||
|
||||
// Let the periodic background job pick up the calendar at a later point
|
||||
$calendar->setLastSync(1);
|
||||
$this->federatedCalendarMapper->update($calendar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ namespace OCA\DAV\CalDAV\AppCalendar;
|
|||
|
||||
use OCA\DAV\CalDAV\CachedSubscriptionImpl;
|
||||
use OCA\DAV\CalDAV\CalendarImpl;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarImpl;
|
||||
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
|
||||
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
|
||||
use OCP\Calendar\IManager;
|
||||
|
|
@ -51,7 +52,11 @@ class AppCalendarPlugin implements ICalendarProvider {
|
|||
return array_values(
|
||||
array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) {
|
||||
// We must not provide a wrapper for DAV calendars
|
||||
return ! (($c instanceof CalendarImpl) || ($c instanceof CachedSubscriptionImpl));
|
||||
return !(
|
||||
($c instanceof CalendarImpl)
|
||||
|| ($c instanceof CachedSubscriptionImpl)
|
||||
|| ($c instanceof FederatedCalendarImpl)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,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;
|
||||
|
|
@ -110,6 +112,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
|
||||
public const CALENDAR_TYPE_CALENDAR = 0;
|
||||
public const CALENDAR_TYPE_SUBSCRIPTION = 1;
|
||||
public const CALENDAR_TYPE_FEDERATED = 2;
|
||||
|
||||
public const PERSONAL_CALENDAR_URI = 'personal';
|
||||
public const PERSONAL_CALENDAR_NAME = 'Personal';
|
||||
|
|
@ -208,6 +211,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,
|
||||
) {
|
||||
}
|
||||
|
|
@ -1408,10 +1412,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
$shares = $this->getShares($calendarId);
|
||||
|
||||
$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow));
|
||||
} else {
|
||||
} elseif ($calendarType === self::CALENDAR_TYPE_SUBSCRIPTION) {
|
||||
$subscriptionRow = $this->getSubscriptionById($calendarId);
|
||||
|
||||
$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
|
||||
} elseif ($calendarType === self::CALENDAR_TYPE_FEDERATED) {
|
||||
// TODO: implement custom event for federated calendars
|
||||
}
|
||||
|
||||
return '"' . $extraData['etag'] . '"';
|
||||
|
|
@ -1468,10 +1474,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
$shares = $this->getShares($calendarId);
|
||||
|
||||
$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow));
|
||||
} else {
|
||||
} elseif ($calendarType === self::CALENDAR_TYPE_SUBSCRIPTION) {
|
||||
$subscriptionRow = $this->getSubscriptionById($calendarId);
|
||||
|
||||
$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow));
|
||||
} elseif ($calendarType === self::CALENDAR_TYPE_FEDERATED) {
|
||||
// TODO: implement custom event for federated calendars
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1978,6 +1986,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
|
||||
if (isset($calendarInfo['source'])) {
|
||||
$calendarType = self::CALENDAR_TYPE_SUBSCRIPTION;
|
||||
} elseif (isset($calendarInfo['federated'])) {
|
||||
$calendarType = self::CALENDAR_TYPE_FEDERATED;
|
||||
} else {
|
||||
$calendarType = self::CALENDAR_TYPE_CALENDAR;
|
||||
}
|
||||
|
|
@ -3197,6 +3207,10 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
return $this->calendarSharingBackend->getShares($resourceId);
|
||||
}
|
||||
|
||||
public function getSharesByShareePrincipal(string $principal): array {
|
||||
return $this->calendarSharingBackend->getSharesByShareePrincipal($principal);
|
||||
}
|
||||
|
||||
public function preloadShares(array $resourceIds): void {
|
||||
$this->calendarSharingBackend->preloadShares($resourceIds);
|
||||
}
|
||||
|
|
@ -3692,4 +3706,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
}
|
||||
}, $this->db);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>[]
|
||||
*/
|
||||
public function getFederatedCalendarsForUser(string $principalUri): array {
|
||||
$federatedCalendars = $this->federatedCalendarMapper->findByPrincipalUri($principalUri);
|
||||
return array_map(
|
||||
static fn (FederatedCalendarEntity $entity) => $entity->toCalendarInfo(),
|
||||
$federatedCalendars,
|
||||
);
|
||||
}
|
||||
|
||||
public function getFederatedCalendarByUri(string $principalUri, string $uri): ?array {
|
||||
$federatedCalendar = $this->federatedCalendarMapper->findByUri($principalUri, $uri);
|
||||
return $federatedCalendar?->toCalendarInfo();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
return $this->calendarInfo['uri'];
|
||||
}
|
||||
|
||||
protected function getCalendarType(): int {
|
||||
return CalDavBackend::CALENDAR_TYPE_CALENDAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @throws Forbidden
|
||||
|
|
@ -197,7 +201,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
$this->getOwner() . '/calendar-proxy-read',
|
||||
$this->getOwner() . '/calendar-proxy-write',
|
||||
parent::getOwner(),
|
||||
'principals/system/public'
|
||||
'principals/system/public',
|
||||
];
|
||||
/** @var list<array{privilege: string, principal: string, protected: bool}> $acl */
|
||||
$acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool {
|
||||
|
|
@ -247,7 +251,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
}
|
||||
|
||||
public function getChild($name) {
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name, $this->getCalendarType());
|
||||
|
||||
if (!$obj) {
|
||||
throw new NotFound('Calendar object not found');
|
||||
|
|
@ -263,7 +267,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
}
|
||||
|
||||
public function getChildren() {
|
||||
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
|
||||
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id'], $this->getCalendarType());
|
||||
$children = [];
|
||||
foreach ($objs as $obj) {
|
||||
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
|
||||
|
|
@ -276,7 +280,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
}
|
||||
|
||||
public function getMultipleChildren(array $paths) {
|
||||
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
|
||||
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths, $this->getCalendarType());
|
||||
$children = [];
|
||||
foreach ($objs as $obj) {
|
||||
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
|
||||
|
|
@ -289,7 +293,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
}
|
||||
|
||||
public function childExists($name) {
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name, $this->getCalendarType());
|
||||
if (!$obj) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -301,7 +305,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
}
|
||||
|
||||
public function calendarQuery(array $filters) {
|
||||
$uris = $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
|
||||
$uris = $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters, $this->getCalendarType());
|
||||
if ($this->isShared()) {
|
||||
return array_filter($uris, function ($uri) {
|
||||
return $this->childExists($uri);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
namespace OCA\DAV\CalDAV;
|
||||
|
||||
use OCA\DAV\AppInfo\PluginManager;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarFactory;
|
||||
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
|
||||
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
|
||||
use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
|
||||
|
|
@ -37,12 +38,14 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
|
|||
|
||||
/** @var PluginManager */
|
||||
private $pluginManager;
|
||||
|
||||
private ?array $cachedChildren = null;
|
||||
|
||||
public function __construct(
|
||||
BackendInterface $caldavBackend,
|
||||
array $principalInfo,
|
||||
private LoggerInterface $logger,
|
||||
private FederatedCalendarFactory $federatedCalendarFactory,
|
||||
private bool $returnCachedSubscriptions,
|
||||
) {
|
||||
parent::__construct($caldavBackend, $principalInfo);
|
||||
|
|
@ -102,6 +105,15 @@ 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[] = $this->federatedCalendarFactory->createFederatedCalendar(
|
||||
$federatedCalendarInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If the backend supports subscriptions, we'll add those as well,
|
||||
|
|
@ -147,13 +159,22 @@ 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 $this->federatedCalendarFactory->createFederatedCalendar($federatedCalendar);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to cover shared calendars
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\DAV\CalDAV;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarImpl;
|
||||
use OCA\DAV\Db\Property;
|
||||
use OCA\DAV\Db\PropertyMapper;
|
||||
use OCP\Calendar\ICalendarProvider;
|
||||
|
|
@ -27,17 +28,24 @@ class CalendarProvider implements ICalendarProvider {
|
|||
}
|
||||
|
||||
public function getCalendars(string $principalUri, array $calendarUris = []): array {
|
||||
|
||||
/** @var array{uri: string, principaluri: string}[] $calendarInfos */
|
||||
$calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri) ?? [];
|
||||
/** @var array{uri: string, principaluri: string}[] $federatedCalendarInfos */
|
||||
$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);
|
||||
});
|
||||
}
|
||||
|
||||
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
|
||||
$iCalendars = [];
|
||||
|
||||
foreach ($calendarInfos as $calendarInfo) {
|
||||
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
|
||||
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
|
||||
|
|
@ -51,6 +59,20 @@ class CalendarProvider implements ICalendarProvider {
|
|||
$this->calDavBackend,
|
||||
);
|
||||
}
|
||||
|
||||
$additionalFederatedProps = $this->getAdditionalPropertiesForCalendars(
|
||||
$federatedCalendarInfos,
|
||||
);
|
||||
foreach ($federatedCalendarInfos as $calendarInfo) {
|
||||
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
|
||||
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
|
||||
if (isset($additionalFederatedProps[$path])) {
|
||||
$calendarInfo = array_merge($calendarInfo, $additionalProperties[$path]);
|
||||
}
|
||||
|
||||
$iCalendars[] = new FederatedCalendarImpl($calendarInfo, $this->calDavBackend);
|
||||
}
|
||||
|
||||
return $iCalendars;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@
|
|||
*/
|
||||
namespace OCA\DAV\CalDAV;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarFactory;
|
||||
use OCA\DAV\CalDAV\Federation\RemoteUserCalendarHome;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\CalDAV\Backend;
|
||||
use Sabre\DAVACL\PrincipalBackend;
|
||||
|
|
@ -19,15 +24,30 @@ class CalendarRoot extends \Sabre\CalDAV\CalendarRoot {
|
|||
Backend\BackendInterface $caldavBackend,
|
||||
$principalPrefix,
|
||||
private LoggerInterface $logger,
|
||||
private IL10N $l10n,
|
||||
private IConfig $config,
|
||||
private FederatedCalendarFactory $federatedCalendarFactory,
|
||||
) {
|
||||
parent::__construct($principalBackend, $caldavBackend, $principalPrefix);
|
||||
}
|
||||
|
||||
public function getChildForPrincipal(array $principal) {
|
||||
[$prefix] = \Sabre\Uri\split($principal['uri']);
|
||||
if ($prefix === RemoteUserPrincipalBackend::PRINCIPAL_PREFIX) {
|
||||
return new RemoteUserCalendarHome(
|
||||
$this->caldavBackend,
|
||||
$principal,
|
||||
$this->l10n,
|
||||
$this->config,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
return new CalendarHome(
|
||||
$this->caldavBackend,
|
||||
$principal,
|
||||
$this->logger,
|
||||
$this->federatedCalendarFactory,
|
||||
array_key_exists($principal['uri'], $this->returnCachedSubscriptions)
|
||||
);
|
||||
}
|
||||
|
|
@ -40,6 +60,10 @@ class CalendarRoot extends \Sabre\CalDAV\CalendarRoot {
|
|||
return $parts[1];
|
||||
}
|
||||
|
||||
if ($this->principalPrefix === RemoteUserPrincipalBackend::PRINCIPAL_PREFIX) {
|
||||
return 'remote-calendars';
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
|
|
|
|||
23
apps/dav/lib/CalDAV/Federation/CalendarFederationConfig.php
Normal file
23
apps/dav/lib/CalDAV/Federation/CalendarFederationConfig.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?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 OCP\AppFramework\Services\IAppConfig;
|
||||
|
||||
class CalendarFederationConfig {
|
||||
public function __construct(
|
||||
private readonly IAppConfig $appConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isFederationEnabled(): bool {
|
||||
return $this->appConfig->getAppValueBool('enableCalendarFederation', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?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 OCP\Federation\ICloudFederationFactory;
|
||||
use OCP\Federation\ICloudFederationProviderManager;
|
||||
use OCP\Federation\ICloudId;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\OCM\Exceptions\OCMProviderException;
|
||||
|
||||
class CalendarFederationNotifier {
|
||||
public const NOTIFICATION_SYNC_CALENDAR = 'SYNC_CALENDAR';
|
||||
|
||||
public const PROP_SYNC_CALENDAR_SHARE_WITH = 'shareWith';
|
||||
public const PROP_SYNC_CALENDAR_CALENDAR_URL = 'calendarUrl';
|
||||
|
||||
public function __construct(
|
||||
private readonly ICloudFederationFactory $federationFactory,
|
||||
private readonly ICloudFederationProviderManager $federationManager,
|
||||
private readonly IURLGenerator $url,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify a remote server to sync a calendar.
|
||||
*
|
||||
* @param ICloudId $shareWith The cloud id of the remote sharee.
|
||||
* @return IResponse
|
||||
*
|
||||
* @throws OCMProviderException If sending the notification fails.
|
||||
*/
|
||||
public function notifySyncCalendar(
|
||||
ICloudId $shareWith,
|
||||
string $calendarOwner,
|
||||
string $calendarName,
|
||||
string $sharedSecret,
|
||||
): IResponse {
|
||||
$sharedWithEncoded = base64_encode($shareWith->getId());
|
||||
$relativeCalendarUrl = "remote-calendars/$sharedWithEncoded/{$calendarName}_shared_by_$calendarOwner";
|
||||
$calendarUrl = $this->url->linkTo('', 'remote.php') . "/dav/$relativeCalendarUrl";
|
||||
$calendarUrl = $this->url->getAbsoluteURL($calendarUrl);
|
||||
|
||||
$notification = $this->federationFactory->getCloudFederationNotification();
|
||||
$notification->setMessage(
|
||||
self::NOTIFICATION_SYNC_CALENDAR,
|
||||
CalendarFederationProvider::CALENDAR_RESOURCE,
|
||||
CalendarFederationProvider::PROVIDER_ID,
|
||||
[
|
||||
'sharedSecret' => $sharedSecret,
|
||||
self::PROP_SYNC_CALENDAR_SHARE_WITH => $shareWith->getId(),
|
||||
self::PROP_SYNC_CALENDAR_CALENDAR_URL => $calendarUrl,
|
||||
],
|
||||
);
|
||||
|
||||
return $this->federationManager->sendCloudNotification(
|
||||
$shareWith->getRemote(),
|
||||
$notification,
|
||||
);
|
||||
}
|
||||
}
|
||||
203
apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php
Normal file
203
apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<?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\BackgroundJob\FederatedCalendarSyncJob;
|
||||
use OCA\DAV\CalDAV\Federation\Protocol\CalendarFederationProtocolV1;
|
||||
use OCA\DAV\CalDAV\Federation\Protocol\ICalendarFederationProtocol;
|
||||
use OCA\DAV\DAV\Sharing\Backend as DavSharingBackend;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\Constants;
|
||||
use OCP\Federation\Exceptions\BadRequestException;
|
||||
use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
|
||||
use OCP\Federation\ICloudFederationProvider;
|
||||
use OCP\Federation\ICloudFederationShare;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Share\Exceptions\ShareNotFound;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class CalendarFederationProvider implements ICloudFederationProvider {
|
||||
public const PROVIDER_ID = 'calendar';
|
||||
public const CALENDAR_RESOURCE = 'calendar';
|
||||
public const USER_SHARE_TYPE = 'user';
|
||||
|
||||
public function __construct(
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly CalendarFederationConfig $calendarFederationConfig,
|
||||
private readonly IJobList $jobList,
|
||||
private readonly ICloudIdManager $cloudIdManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getShareType(): string {
|
||||
return self::PROVIDER_ID;
|
||||
}
|
||||
|
||||
public function shareReceived(ICloudFederationShare $share): string {
|
||||
if (!$this->calendarFederationConfig->isFederationEnabled()) {
|
||||
$this->logger->debug('Received a federation invite but federation is disabled');
|
||||
throw new ProviderCouldNotAddShareException(
|
||||
'Server does not support calendar 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,
|
||||
);
|
||||
// TODO: Implement group shares
|
||||
}
|
||||
|
||||
$rawProtocol = $share->getProtocol();
|
||||
switch ($rawProtocol[ICalendarFederationProtocol::PROP_VERSION]) {
|
||||
case CalendarFederationProtocolV1::VERSION:
|
||||
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();
|
||||
$access = $protocol->getAccess();
|
||||
$components = $protocol->getComponents();
|
||||
break;
|
||||
default:
|
||||
throw new ProviderCouldNotAddShareException(
|
||||
'Unknown protocol version',
|
||||
'',
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$calendarUrl || !$displayName) {
|
||||
throw new ProviderCouldNotAddShareException(
|
||||
'Incomplete protocol data',
|
||||
'',
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: implement read-write sharing
|
||||
$permissions = match ($access) {
|
||||
DavSharingBackend::ACCESS_READ => Constants::PERMISSION_READ,
|
||||
default => throw new ProviderCouldNotAddShareException(
|
||||
"Unsupported access value: $access",
|
||||
'',
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
),
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
||||
// Delete existing incoming federated share first
|
||||
$this->federatedCalendarMapper->deleteByUri($sharedWithPrincipal, $calendarUri);
|
||||
|
||||
$calendar = new FederatedCalendarEntity();
|
||||
$calendar->setPrincipaluri($sharedWithPrincipal);
|
||||
$calendar->setUri($calendarUri);
|
||||
$calendar->setRemoteUrl($calendarUrl);
|
||||
$calendar->setDisplayName($displayName);
|
||||
$calendar->setColor($color);
|
||||
$calendar->setToken($share->getShareSecret());
|
||||
$calendar->setSharedBy($share->getSharedBy());
|
||||
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
|
||||
$calendar->setPermissions($permissions);
|
||||
$calendar->setComponents($components);
|
||||
$calendar = $this->federatedCalendarMapper->insert($calendar);
|
||||
|
||||
$this->jobList->add(FederatedCalendarSyncJob::class, [
|
||||
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
|
||||
]);
|
||||
|
||||
return (string)$calendar->getId();
|
||||
}
|
||||
|
||||
public function notificationReceived(
|
||||
$notificationType,
|
||||
$providerId,
|
||||
array $notification,
|
||||
): array {
|
||||
if ($providerId !== self::PROVIDER_ID) {
|
||||
throw new BadRequestException(['providerId']);
|
||||
}
|
||||
|
||||
switch ($notificationType) {
|
||||
case CalendarFederationNotifier::NOTIFICATION_SYNC_CALENDAR:
|
||||
return $this->handleSyncCalendarNotification($notification);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSupportedShareTypes(): array {
|
||||
return [self::USER_SHARE_TYPE];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequestException If notification props are missing.
|
||||
* @throws ShareNotFound If the notification is not related to a known share.
|
||||
*/
|
||||
private function handleSyncCalendarNotification(array $notification): array {
|
||||
$sharedSecret = $notification['sharedSecret'];
|
||||
$shareWithRaw = $notification[CalendarFederationNotifier::PROP_SYNC_CALENDAR_SHARE_WITH] ?? null;
|
||||
$calendarUrl = $notification[CalendarFederationNotifier::PROP_SYNC_CALENDAR_CALENDAR_URL] ?? null;
|
||||
|
||||
if ($shareWithRaw === null || $shareWithRaw === '') {
|
||||
throw new BadRequestException([CalendarFederationNotifier::PROP_SYNC_CALENDAR_SHARE_WITH]);
|
||||
}
|
||||
|
||||
if ($calendarUrl === null || $calendarUrl === '') {
|
||||
throw new BadRequestException([CalendarFederationNotifier::PROP_SYNC_CALENDAR_CALENDAR_URL]);
|
||||
}
|
||||
|
||||
try {
|
||||
$shareWith = $this->cloudIdManager->resolveCloudId($shareWithRaw);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new ShareNotFound('Invalid sharee cloud id');
|
||||
}
|
||||
|
||||
$calendars = $this->federatedCalendarMapper->findByRemoteUrl(
|
||||
$calendarUrl,
|
||||
'principals/users/' . $shareWith->getUser(),
|
||||
$sharedSecret,
|
||||
);
|
||||
if (empty($calendars)) {
|
||||
throw new ShareNotFound('Calendar is not shared with the sharee');
|
||||
}
|
||||
|
||||
foreach ($calendars as $calendar) {
|
||||
$this->jobList->add(FederatedCalendarSyncJob::class, [
|
||||
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
38
apps/dav/lib/CalDAV/Federation/FederatedCalendar.php
Normal file
38
apps/dav/lib/CalDAV/Federation/FederatedCalendar.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?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 extends Calendar {
|
||||
public function __construct(
|
||||
Backend\BackendInterface $caldavBackend,
|
||||
$calendarInfo,
|
||||
IL10N $l10n,
|
||||
IConfig $config,
|
||||
LoggerInterface $logger,
|
||||
private readonly 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;
|
||||
}
|
||||
}
|
||||
81
apps/dav/lib/CalDAV/Federation/FederatedCalendarAuth.php
Normal file
81
apps/dav/lib/CalDAV/Federation/FederatedCalendarAuth.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?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\DAV\RemoteUserPrincipalBackend;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\Defaults;
|
||||
use Sabre\DAV\Auth\Backend\BackendInterface;
|
||||
use Sabre\HTTP\Auth\Basic as BasicAuth;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
|
||||
class FederatedCalendarAuth implements BackendInterface {
|
||||
private readonly string $realm;
|
||||
|
||||
public function __construct(
|
||||
private readonly SharingMapper $sharingMapper,
|
||||
) {
|
||||
$defaults = new Defaults();
|
||||
$this->realm = $defaults->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null A principal uri if the given combination of user and pass is valid and null otherwise.
|
||||
*/
|
||||
private function validateUserPass(
|
||||
string $requestPath,
|
||||
string $username,
|
||||
string $password,
|
||||
): ?string {
|
||||
$remoteUserPrincipalUri = RemoteUserPrincipalBackend::PRINCIPAL_PREFIX . "/$username";
|
||||
[, $remoteUserPrincipalId] = \Sabre\Uri\split($remoteUserPrincipalUri);
|
||||
|
||||
$rows = $this->sharingMapper->getSharedCalendarsForRemoteUser(
|
||||
$remoteUserPrincipalUri,
|
||||
$password,
|
||||
);
|
||||
|
||||
// Is the requested calendar actually shared with the remote user?
|
||||
foreach ($rows as $row) {
|
||||
$ownerPrincipalUri = $row['principaluri'];
|
||||
[, $ownerUserId] = \Sabre\Uri\split($ownerPrincipalUri);
|
||||
$shareUri = $row['uri'] . '_shared_by_' . $ownerUserId;
|
||||
if (str_starts_with($requestPath, "remote-calendars/$remoteUserPrincipalId/$shareUri")) {
|
||||
// Yes? -> return early
|
||||
return $remoteUserPrincipalUri;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function check(RequestInterface $request, ResponseInterface $response): array {
|
||||
if (!str_starts_with($request->getPath(), 'remote-calendars/')) {
|
||||
return [false, 'This request is not for a federated calendar'];
|
||||
}
|
||||
|
||||
$auth = new BasicAuth($this->realm, $request, $response);
|
||||
$userpass = $auth->getCredentials();
|
||||
if ($userpass === null || count($userpass) !== 2) {
|
||||
return [false, "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured"];
|
||||
}
|
||||
$principal = $this->validateUserPass($request->getPath(), $userpass[0], $userpass[1]);
|
||||
if ($principal === null) {
|
||||
return [false, 'Username or password was incorrect'];
|
||||
}
|
||||
|
||||
return [true, $principal];
|
||||
}
|
||||
|
||||
public function challenge(RequestInterface $request, ResponseInterface $response): void {
|
||||
// No special challenge is needed here
|
||||
}
|
||||
}
|
||||
101
apps/dav/lib/CalDAV/Federation/FederatedCalendarEntity.php
Normal file
101
apps/dav/lib/CalDAV/Federation/FederatedCalendarEntity.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?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\DAV\RemoteUserPrincipalBackend;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\DB\Types;
|
||||
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
|
||||
|
||||
/**
|
||||
* @method string getPrincipaluri()
|
||||
* @method void setPrincipaluri(string $principaluri)
|
||||
* @method string getUri()
|
||||
* @method void setUri(string $uri)
|
||||
* @method string getDisplayName()
|
||||
* @method void setDisplayName(string $displayName)
|
||||
* @method string|null getColor()
|
||||
* @method void setColor(string|null $color)
|
||||
* @method int getPermissions()
|
||||
* @method void setPermissions(int $permissions)
|
||||
* @method int getSyncToken()
|
||||
* @method void setSyncToken(int $syncToken)
|
||||
* @method string getRemoteUrl()
|
||||
* @method void setRemoteUrl(string $remoteUrl)
|
||||
* @method string getToken()
|
||||
* @method void setToken(string $token)
|
||||
* @method int|null getLastSync()
|
||||
* @method void setLastSync(int|null $lastSync)
|
||||
* @method string getSharedBy()
|
||||
* @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 = '';
|
||||
protected string $uri = '';
|
||||
protected string $displayName = '';
|
||||
protected ?string $color = null;
|
||||
protected int $permissions = 0;
|
||||
protected int $syncToken = 0;
|
||||
protected string $remoteUrl = '';
|
||||
protected string $token = '';
|
||||
protected ?int $lastSync = null;
|
||||
protected string $sharedBy = '';
|
||||
protected string $sharedByDisplayName = '';
|
||||
protected string $components = '';
|
||||
|
||||
public function __construct() {
|
||||
$this->addType('principaluri', Types::STRING);
|
||||
$this->addType('uri', Types::STRING);
|
||||
$this->addType('color', Types::STRING);
|
||||
$this->addType('displayName', Types::STRING);
|
||||
$this->addType('permissions', Types::INTEGER);
|
||||
$this->addType('syncToken', Types::INTEGER);
|
||||
$this->addType('remoteUrl', Types::STRING);
|
||||
$this->addType('token', Types::STRING);
|
||||
$this->addType('lastSync', Types::INTEGER);
|
||||
$this->addType('sharedBy', Types::STRING);
|
||||
$this->addType('sharedByDisplayName', Types::STRING);
|
||||
$this->addType('components', Types::STRING);
|
||||
}
|
||||
|
||||
public function getSyncTokenForSabre(): string {
|
||||
return 'http://sabre.io/ns/sync/' . $this->getSyncToken();
|
||||
}
|
||||
|
||||
public function getSharedByPrincipal(): string {
|
||||
return RemoteUserPrincipalBackend::PRINCIPAL_PREFIX . '/' . 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' => 1,
|
||||
|
||||
'{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
|
||||
];
|
||||
}
|
||||
}
|
||||
42
apps/dav/lib/CalDAV/Federation/FederatedCalendarFactory.php
Normal file
42
apps/dav/lib/CalDAV/Federation/FederatedCalendarFactory.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?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\AppInfo\Application;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory as IL10NFactory;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FederatedCalendarFactory {
|
||||
private readonly IL10N $l10n;
|
||||
|
||||
public function __construct(
|
||||
private readonly CalDavBackend $caldavBackend,
|
||||
private readonly IConfig $config,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
IL10NFactory $l10nFactory,
|
||||
) {
|
||||
$this->l10n = $l10nFactory->get(Application::APP_ID);
|
||||
}
|
||||
|
||||
public function createFederatedCalendar(array $calendarInfo): FederatedCalendar {
|
||||
return new FederatedCalendar(
|
||||
$this->caldavBackend,
|
||||
$calendarInfo,
|
||||
$this->l10n,
|
||||
$this->config,
|
||||
$this->logger,
|
||||
$this->federatedCalendarMapper,
|
||||
);
|
||||
}
|
||||
}
|
||||
73
apps/dav/lib/CalDAV/Federation/FederatedCalendarImpl.php
Normal file
73
apps/dav/lib/CalDAV/Federation/FederatedCalendarImpl.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?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\Constants;
|
||||
|
||||
class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIsWritable, ICalendarIsEnabled {
|
||||
public function __construct(
|
||||
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;
|
||||
}
|
||||
}
|
||||
209
apps/dav/lib/CalDAV/Federation/FederatedCalendarMapper.php
Normal file
209
apps/dav/lib/CalDAV/Federation/FederatedCalendarMapper.php
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<?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 OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
/** @template-extends QBMapper<FederatedCalendarEntity> */
|
||||
class FederatedCalendarMapper extends QBMapper {
|
||||
public const TABLE_NAME = 'calendars_federated';
|
||||
|
||||
public function __construct(
|
||||
IDBConnection $db,
|
||||
private readonly ITimeFactory $time,
|
||||
) {
|
||||
parent::__construct($db, self::TABLE_NAME, FederatedCalendarEntity::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DoesNotExistException If there is no federated calendar with the given id.
|
||||
*/
|
||||
public function find(int $id): FederatedCalendarEntity {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where($qb->expr()->eq(
|
||||
'id',
|
||||
$qb->createNamedParameter($id, IQueryBuilder::PARAM_INT),
|
||||
IQueryBuilder::PARAM_INT,
|
||||
));
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FederatedCalendarEntity[]
|
||||
*/
|
||||
public function findByPrincipalUri(string $principalUri): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where($qb->expr()->eq(
|
||||
'principaluri',
|
||||
$qb->createNamedParameter($principalUri, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
));
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function findByUri(string $principalUri, string $uri): ?FederatedCalendarEntity {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where($qb->expr()->eq(
|
||||
'principaluri',
|
||||
$qb->createNamedParameter($principalUri, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
))
|
||||
->andWhere($qb->expr()->eq(
|
||||
'uri',
|
||||
$qb->createNamedParameter($uri, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
));
|
||||
|
||||
try {
|
||||
return $this->findEntity($qb);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return null;
|
||||
} catch (MultipleObjectsReturnedException $e) {
|
||||
// Should never happen
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FederatedCalendarEntity[]
|
||||
*/
|
||||
public function findUnsyncedSinceBefore(int $beforeTimestamp): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where($qb->expr()->lt(
|
||||
'last_sync',
|
||||
$qb->createNamedParameter($beforeTimestamp, IQueryBuilder::PARAM_INT),
|
||||
IQueryBuilder::PARAM_INT,
|
||||
))
|
||||
// Omit unsynced calendars for now as they are synced by a separate job
|
||||
->andWhere($qb->expr()->isNotNull('last_sync'));
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function deleteById(int $id): void {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete(self::TABLE_NAME)
|
||||
->where($qb->expr()->eq(
|
||||
'id',
|
||||
$qb->createNamedParameter($id, IQueryBuilder::PARAM_INT),
|
||||
IQueryBuilder::PARAM_INT,
|
||||
));
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
public function updateSyncTime(int $id): void {
|
||||
$now = $this->time->getTime();
|
||||
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->update(self::TABLE_NAME)
|
||||
->set('last_sync', $qb->createNamedParameter($now, IQueryBuilder::PARAM_INT))
|
||||
->where($qb->expr()->eq(
|
||||
'id',
|
||||
$qb->createNamedParameter($id, IQueryBuilder::PARAM_INT),
|
||||
IQueryBuilder::PARAM_INT,
|
||||
));
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
public function updateSyncTokenAndTime(int $id, int $syncToken): void {
|
||||
$now = $this->time->getTime();
|
||||
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->update(self::TABLE_NAME)
|
||||
->set('sync_token', $qb->createNamedParameter($syncToken, IQueryBuilder::PARAM_INT))
|
||||
->set('last_sync', $qb->createNamedParameter($now, IQueryBuilder::PARAM_INT))
|
||||
->where($qb->expr()->eq(
|
||||
'id',
|
||||
$qb->createNamedParameter($id, IQueryBuilder::PARAM_INT),
|
||||
IQueryBuilder::PARAM_INT,
|
||||
));
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<mixed, FederatedCalendarEntity>
|
||||
*/
|
||||
public function findAll(): \Generator {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from(self::TABLE_NAME);
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
while ($row = $result->fetch()) {
|
||||
yield $this->mapRowToEntity($row);
|
||||
}
|
||||
$result->closeCursor();
|
||||
}
|
||||
|
||||
public function countAll(): int {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select($qb->func()->count('*'))
|
||||
->from(self::TABLE_NAME);
|
||||
$result = $qb->executeQuery();
|
||||
$count = (int)$result->fetchOne();
|
||||
$result->closeCursor();
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function deleteByUri(string $principalUri, string $uri): void {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete(self::TABLE_NAME)
|
||||
->where($qb->expr()->eq(
|
||||
'principaluri',
|
||||
$qb->createNamedParameter($principalUri, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
))
|
||||
->andWhere($qb->expr()->eq(
|
||||
'uri',
|
||||
$qb->createNamedParameter($uri, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
));
|
||||
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FederatedCalendarEntity[]
|
||||
*/
|
||||
public function findByRemoteUrl(string $remoteUrl, string $principalUri, string $token): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where($qb->expr()->eq(
|
||||
'remote_url',
|
||||
$qb->createNamedParameter($remoteUrl, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
))
|
||||
->andWhere($qb->expr()->eq(
|
||||
'principaluri',
|
||||
$qb->createNamedParameter($principalUri, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
))
|
||||
->andWhere($qb->expr()->eq(
|
||||
'token',
|
||||
$qb->createNamedParameter($token, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
));
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?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\SyncService as CalDavSyncService;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FederatedCalendarSyncService {
|
||||
private const SYNC_TOKEN_PREFIX = 'http://sabre.io/ns/sync/';
|
||||
|
||||
public function __construct(
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly CalDavSyncService $syncService,
|
||||
private readonly ICloudIdManager $cloudIdManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Downloaded event count (created or updated).
|
||||
*
|
||||
* @throws ClientExceptionInterface If syncing the calendar fails.
|
||||
*/
|
||||
public function syncOne(FederatedCalendarEntity $calendar): int {
|
||||
[,, $sharedWith] = explode('/', $calendar->getPrincipaluri());
|
||||
$calDavUser = $this->cloudIdManager->getCloudId($sharedWith, null)->getId();
|
||||
$remoteUrl = $calendar->getRemoteUrl();
|
||||
$syncToken = $calendar->getSyncTokenForSabre();
|
||||
|
||||
// Need to encode the cloud id as it might contain a colon which is not allowed in basic
|
||||
// auth according to RFC 7617
|
||||
$calDavUser = base64_encode($calDavUser);
|
||||
|
||||
$syncResponse = $this->syncService->syncRemoteCalendar(
|
||||
$remoteUrl,
|
||||
$calDavUser,
|
||||
$calendar->getToken(),
|
||||
$syncToken,
|
||||
$calendar,
|
||||
);
|
||||
|
||||
$newSyncToken = $syncResponse->getSyncToken();
|
||||
|
||||
// Check sync token format and extract the actual sync token integer
|
||||
$matches = [];
|
||||
if (!preg_match('/^http:\/\/sabre\.io\/ns\/sync\/([0-9]+)$/', $newSyncToken, $matches)) {
|
||||
$this->logger->error("Failed to sync federated calendar at $remoteUrl: New sync token has unexpected format: $newSyncToken", [
|
||||
'calendar' => $calendar->toCalendarInfo(),
|
||||
'newSyncToken' => $newSyncToken,
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$newSyncToken = (int)$matches[1];
|
||||
if ($newSyncToken !== $calendar->getSyncToken()) {
|
||||
$this->federatedCalendarMapper->updateSyncTokenAndTime(
|
||||
$calendar->getId(),
|
||||
$newSyncToken,
|
||||
);
|
||||
} else {
|
||||
$this->logger->debug("Sync Token for $remoteUrl unchanged from previous sync");
|
||||
$this->federatedCalendarMapper->updateSyncTime($calendar->getId());
|
||||
}
|
||||
|
||||
return $syncResponse->getDownloadedEvents();
|
||||
}
|
||||
}
|
||||
180
apps/dav/lib/CalDAV/Federation/FederationSharingService.php
Normal file
180
apps/dav/lib/CalDAV/Federation/FederationSharingService.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?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\Federation\Protocol\CalendarFederationProtocolV1;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCA\DAV\DAV\Sharing\IShareable;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\Federation\ICloudFederationFactory;
|
||||
use OCP\Federation\ICloudFederationProviderManager;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserManager;
|
||||
use OCP\OCM\Exceptions\OCMProviderException;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\CalDAV\Calendar;
|
||||
|
||||
// TODO: Convert this to an abstract service like the addressbook/calendar sharing services once we
|
||||
// support addressbook federation as well.
|
||||
class FederationSharingService {
|
||||
public function __construct(
|
||||
private readonly ICloudFederationProviderManager $federationManager,
|
||||
private readonly ICloudFederationFactory $federationFactory,
|
||||
private readonly IUserManager $userManager,
|
||||
private readonly IURLGenerator $url,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly ISecureRandom $random,
|
||||
private readonly SharingMapper $sharingMapper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a (base64) encoded remote user principal and return the remote user's cloud id. Will
|
||||
* return null if the given principal is not belonging to a remote user (or has an invalid
|
||||
* format).
|
||||
*
|
||||
* The remote user/cloud id needs to be encoded as it might contain slashes.
|
||||
*/
|
||||
private function decodeRemoteUserPrincipal(string $principal): ?string {
|
||||
// Expected format: principals/remote-users/abcdef123
|
||||
[$prefix, $collection, $encodedId] = explode('/', $principal);
|
||||
if ($prefix !== 'principals' || $collection !== 'remote-users') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decodedId = base64_decode($encodedId);
|
||||
if (!is_string($decodedId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $decodedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a calendar share to a remote instance and create a federated share locally if it is
|
||||
* accepted.
|
||||
*
|
||||
* @param IShareable $shareable The calendar to be shared.
|
||||
* @param string $principal The principal to share with (should be a remote user principal).
|
||||
* @param int $access The access level. The remote serve might reject it.
|
||||
*/
|
||||
public function shareWith(IShareable $shareable, string $principal, int $access): void {
|
||||
$baseError = 'Failed to create federated calendar share: ';
|
||||
|
||||
// 1. Validate share data
|
||||
$shareWith = $this->decodeRemoteUserPrincipal($principal);
|
||||
if ($shareWith === null) {
|
||||
$this->logger->error($baseError . 'Principal of sharee is not belonging to a remote user', [
|
||||
'shareable' => $shareable->getName(),
|
||||
'encodedShareWith' => $principal,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
[,, $ownerUid] = explode('/', $shareable->getOwner());
|
||||
$owner = $this->userManager->get($ownerUid);
|
||||
if ($owner === null) {
|
||||
$this->logger->error($baseError . 'Shareable is not owned by a user on this server', [
|
||||
'shareable' => $shareable->getName(),
|
||||
'shareWith' => $shareWith,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Need a calendar instance to extract properties for the protocol
|
||||
$calendar = $shareable;
|
||||
if (!($calendar instanceof Calendar)) {
|
||||
$this->logger->error($baseError . 'Shareable is not a calendar', [
|
||||
'shareable' => $shareable->getName(),
|
||||
'owner' => $owner,
|
||||
'shareWith' => $shareWith,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$getProp = static fn (string $prop) => $calendar->getProperties([$prop])[$prop] ?? null;
|
||||
|
||||
$displayName = $getProp('{DAV:}displayname') ?? '';
|
||||
|
||||
$token = $this->random->generate(32);
|
||||
$share = $this->federationFactory->getCloudFederationShare(
|
||||
$shareWith,
|
||||
$shareable->getName(),
|
||||
$displayName,
|
||||
CalendarFederationProvider::PROVIDER_ID,
|
||||
// Resharing is not possible so the owner is always the sharer
|
||||
$owner->getCloudId(),
|
||||
$owner->getDisplayName(),
|
||||
$owner->getCloudId(),
|
||||
$owner->getDisplayName(),
|
||||
$token,
|
||||
CalendarFederationProvider::USER_SHARE_TYPE,
|
||||
CalendarFederationProvider::CALENDAR_RESOURCE,
|
||||
);
|
||||
|
||||
// 2. Send share to federated instance
|
||||
$shareWithEncoded = base64_encode($shareWith);
|
||||
$relativeCalendarUrl = "remote-calendars/$shareWithEncoded/" . $calendar->getName() . '_shared_by_' . $ownerUid;
|
||||
$calendarUrl = $this->url->linkTo('', 'remote.php') . "/dav/$relativeCalendarUrl";
|
||||
$calendarUrl = $this->url->getAbsoluteURL($calendarUrl);
|
||||
$protocol = new CalendarFederationProtocolV1();
|
||||
$protocol->setUrl($calendarUrl);
|
||||
$protocol->setDisplayName($displayName);
|
||||
$protocol->setColor($getProp('{http://apple.com/ns/ical/}calendar-color'));
|
||||
$protocol->setAccess($access);
|
||||
$protocol->setComponents(implode(',', $getProp(
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set')?->getValue() ?? [],
|
||||
));
|
||||
$share->setProtocol([
|
||||
// Preserve original protocol contents
|
||||
...$share->getProtocol(),
|
||||
...$protocol->toProtocol(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$response = $this->federationManager->sendCloudShare($share);
|
||||
} catch (OCMProviderException $e) {
|
||||
$this->logger->error($baseError . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'owner' => $owner->getUID(),
|
||||
'calendar' => $shareable->getName(),
|
||||
'shareWith' => $shareWith,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() !== Http::STATUS_CREATED) {
|
||||
$this->logger->error($baseError . 'Server replied with code ' . $response->getStatusCode(), [
|
||||
'responseBody' => $response->getBody(),
|
||||
'owner' => $owner->getUID(),
|
||||
'calendar' => $shareable->getName(),
|
||||
'shareWith' => $shareWith,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Create a local DAV share to track the token for authentication
|
||||
$shareWithPrincipalUri = RemoteUserPrincipalBackend::PRINCIPAL_PREFIX . '/' . $shareWithEncoded;
|
||||
$this->sharingMapper->deleteShare(
|
||||
$shareable->getResourceId(),
|
||||
'calendar',
|
||||
$shareWithPrincipalUri,
|
||||
);
|
||||
$this->sharingMapper->shareWithToken(
|
||||
$shareable->getResourceId(),
|
||||
'calendar',
|
||||
$access,
|
||||
$shareWithPrincipalUri,
|
||||
$token,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<?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\Protocol;
|
||||
|
||||
class CalendarFederationProtocolV1 implements ICalendarFederationProtocol {
|
||||
public const VERSION = 'v1';
|
||||
|
||||
public const PROP_URL = 'url';
|
||||
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.
|
||||
*/
|
||||
public static function parse(array $rawProtocol): self {
|
||||
if ($rawProtocol[self::PROP_VERSION] !== self::VERSION) {
|
||||
throw new CalendarProtocolParseException('Unknown protocol version');
|
||||
}
|
||||
|
||||
$url = $rawProtocol[self::PROP_URL] ?? null;
|
||||
if (!is_string($url)) {
|
||||
throw new CalendarProtocolParseException('URL is missing or not a string');
|
||||
}
|
||||
|
||||
$displayName = $rawProtocol[self::PROP_DISPLAY_NAME] ?? null;
|
||||
if (!is_string($displayName)) {
|
||||
throw new CalendarProtocolParseException('Display name is missing or not a string');
|
||||
}
|
||||
|
||||
$color = $rawProtocol[self::PROP_COLOR] ?? null;
|
||||
if (!is_string($color) && $color !== null) {
|
||||
throw new CalendarProtocolParseException('Color is set but not a string');
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function toProtocol(): array {
|
||||
return [
|
||||
self::PROP_VERSION => $this->getVersion(),
|
||||
self::PROP_URL => $this->getUrl(),
|
||||
self::PROP_DISPLAY_NAME => $this->getDisplayName(),
|
||||
self::PROP_COLOR => $this->getColor(),
|
||||
self::PROP_ACCESS => $this->getAccess(),
|
||||
self::PROP_COMPONENTS => $this->getComponents(),
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getVersion(): string {
|
||||
return self::VERSION;
|
||||
}
|
||||
|
||||
public function getUrl(): string {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function setUrl(string $url): void {
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function getDisplayName(): string {
|
||||
return $this->displayName;
|
||||
}
|
||||
|
||||
public function setDisplayName(string $displayName): void {
|
||||
$this->displayName = $displayName;
|
||||
}
|
||||
|
||||
public function getColor(): ?string {
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function setColor(?string $color): void {
|
||||
$this->color = $color;
|
||||
}
|
||||
|
||||
public function getAccess(): int {
|
||||
return $this->access;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?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\Protocol;
|
||||
|
||||
class CalendarProtocolParseException extends \Exception {
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?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\Protocol;
|
||||
|
||||
interface ICalendarFederationProtocol {
|
||||
public const PROP_VERSION = 'version';
|
||||
|
||||
/**
|
||||
* Get the version of this protocol implementation.
|
||||
*/
|
||||
public function getVersion(): string;
|
||||
|
||||
/**
|
||||
* Convert the protocol to an associative array to be sent to a remote instance.
|
||||
* The resulting array still needs to be merged with the base protocol from the share!
|
||||
*/
|
||||
public function toProtocol(): array;
|
||||
}
|
||||
67
apps/dav/lib/CalDAV/Federation/RemoteUserCalendarHome.php
Normal file
67
apps/dav/lib/CalDAV/Federation/RemoteUserCalendarHome.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?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\Calendar;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\CalDAV\Backend;
|
||||
use Sabre\CalDAV\CalendarHome;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
|
||||
class RemoteUserCalendarHome extends CalendarHome {
|
||||
public function __construct(
|
||||
Backend\BackendInterface $caldavBackend,
|
||||
$principalInfo,
|
||||
private readonly IL10N $l10n,
|
||||
private readonly IConfig $config,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct($caldavBackend, $principalInfo);
|
||||
}
|
||||
|
||||
public function getChild($name) {
|
||||
// Remote users can only have incoming shared calendars so we can skip the rest of a regular
|
||||
// calendar home
|
||||
foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) {
|
||||
if ($calendar['uri'] === $name) {
|
||||
return new Calendar(
|
||||
$this->caldavBackend,
|
||||
$calendar,
|
||||
$this->l10n,
|
||||
$this->config,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFound("Node with name $name could not be found");
|
||||
}
|
||||
|
||||
public function getChildren(): array {
|
||||
$objects = [];
|
||||
|
||||
// Remote users can only have incoming shared calendars so we can skip the rest of a regular
|
||||
// calendar home
|
||||
$calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
|
||||
foreach ($calendars as $calendar) {
|
||||
$objects[] = new Calendar(
|
||||
$this->caldavBackend,
|
||||
$calendar,
|
||||
$this->l10n,
|
||||
$this->config,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
return $objects;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\DAV\CalDAV\Sharing;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederationSharingService;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCA\DAV\DAV\Sharing\Backend as SharingBackend;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IGroupManager;
|
||||
|
|
@ -21,10 +23,12 @@ class Backend extends SharingBackend {
|
|||
private IUserManager $userManager,
|
||||
private IGroupManager $groupManager,
|
||||
private Principal $principalBackend,
|
||||
private RemoteUserPrincipalBackend $remoteUserPrincipalBackend,
|
||||
private ICacheFactory $cacheFactory,
|
||||
private Service $service,
|
||||
private FederationSharingService $federationSharingService,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->cacheFactory, $this->service, $this->logger);
|
||||
parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->remoteUserPrincipalBackend, $this->cacheFactory, $this->service, $this->federationSharingService, $this->logger);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
apps/dav/lib/CalDAV/SyncService.php
Normal file
92
apps/dav/lib/CalDAV/SyncService.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?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;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\Service\ASyncService;
|
||||
use OCP\AppFramework\Db\TTransactional;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class SyncService extends ASyncService {
|
||||
use TTransactional;
|
||||
|
||||
public function __construct(
|
||||
IClientService $clientService,
|
||||
IConfig $config,
|
||||
private readonly CalDavBackend $backend,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly IDBConnection $dbConnection,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct($clientService, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $username
|
||||
* @param string $sharedSecret
|
||||
* @param string|null $syncToken
|
||||
* @param FederatedCalendarEntity $calendar
|
||||
*/
|
||||
public function syncRemoteCalendar(
|
||||
string $url,
|
||||
string $username,
|
||||
string $sharedSecret,
|
||||
?string $syncToken,
|
||||
FederatedCalendarEntity $calendar,
|
||||
): SyncServiceResult {
|
||||
try {
|
||||
$response = $this->requestSyncReport($url, $username, $sharedSecret, $syncToken);
|
||||
} catch (ClientExceptionInterface $ex) {
|
||||
if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
|
||||
// Remote server revoked access to the calendar => remove it
|
||||
$this->federatedCalendarMapper->delete($calendar);
|
||||
$this->logger->error("Authorization failed, remove federated calendar: $url", [
|
||||
'app' => 'dav',
|
||||
]);
|
||||
throw $ex;
|
||||
}
|
||||
$this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]);
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
// TODO: use multi-get for download
|
||||
$downloadedEvents = 0;
|
||||
foreach ($response['response'] as $resource => $status) {
|
||||
$objectUri = basename($resource);
|
||||
if (isset($status[200])) {
|
||||
$absoluteUrl = $this->prepareUri($url, $resource);
|
||||
$vCard = $this->download($absoluteUrl, $username, $sharedSecret);
|
||||
$this->atomic(function () use ($calendar, $objectUri, $vCard): void {
|
||||
$existingObject = $this->backend->getCalendarObject($calendar->getId(), $objectUri, CalDavBackend::CALENDAR_TYPE_FEDERATED);
|
||||
if (!$existingObject) {
|
||||
$this->backend->createCalendarObject($calendar->getId(), $objectUri, $vCard, CalDavBackend::CALENDAR_TYPE_FEDERATED);
|
||||
} else {
|
||||
$this->backend->updateCalendarObject($calendar->getId(), $objectUri, $vCard, CalDavBackend::CALENDAR_TYPE_FEDERATED);
|
||||
}
|
||||
}, $this->dbConnection);
|
||||
$downloadedEvents++;
|
||||
} else {
|
||||
$this->backend->deleteCalendarObject($calendar->getId(), $objectUri, CalDavBackend::CALENDAR_TYPE_FEDERATED, true);
|
||||
}
|
||||
}
|
||||
|
||||
return new SyncServiceResult(
|
||||
$response['token'],
|
||||
$downloadedEvents,
|
||||
);
|
||||
}
|
||||
}
|
||||
26
apps/dav/lib/CalDAV/SyncServiceResult.php
Normal file
26
apps/dav/lib/CalDAV/SyncServiceResult.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?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;
|
||||
|
||||
final class SyncServiceResult {
|
||||
public function __construct(
|
||||
private readonly string $syncToken,
|
||||
private readonly int $downloadedEvents,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSyncToken(): string {
|
||||
return $this->syncToken;
|
||||
}
|
||||
|
||||
public function getDownloadedEvents(): int {
|
||||
return $this->downloadedEvents;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\DAV\CardDAV\Sharing;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederationSharingService;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCA\DAV\DAV\Sharing\Backend as SharingBackend;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IGroupManager;
|
||||
|
|
@ -20,10 +22,12 @@ class Backend extends SharingBackend {
|
|||
private IUserManager $userManager,
|
||||
private IGroupManager $groupManager,
|
||||
private Principal $principalBackend,
|
||||
private RemoteUserPrincipalBackend $remoteUserPrincipalBackend,
|
||||
private ICacheFactory $cacheFactory,
|
||||
private Service $service,
|
||||
private FederationSharingService $federationSharingService,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->cacheFactory, $this->service, $this->logger);
|
||||
parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->remoteUserPrincipalBackend, $this->cacheFactory, $this->service, $this->federationSharingService, $this->logger);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
*/
|
||||
namespace OCA\DAV\CardDAV;
|
||||
|
||||
use OCA\DAV\Service\ASyncService;
|
||||
use OCP\AppFramework\Db\TTransactional;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\Http\Client\IClient;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
|
|
@ -19,27 +19,26 @@ use OCP\IUser;
|
|||
use OCP\IUserManager;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Xml\Response\MultiStatus;
|
||||
use Sabre\DAV\Xml\Service;
|
||||
use Sabre\VObject\Reader;
|
||||
use Sabre\Xml\ParseException;
|
||||
use function is_null;
|
||||
|
||||
class SyncService {
|
||||
class SyncService extends ASyncService {
|
||||
|
||||
use TTransactional;
|
||||
private ?array $localSystemAddressBook = null;
|
||||
protected string $certPath;
|
||||
|
||||
public function __construct(
|
||||
IClientService $clientService,
|
||||
IConfig $config,
|
||||
private CardDavBackend $backend,
|
||||
private IUserManager $userManager,
|
||||
private IDBConnection $dbConnection,
|
||||
private LoggerInterface $logger,
|
||||
private Converter $converter,
|
||||
private IClientService $clientService,
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct($clientService, $config);
|
||||
|
||||
$this->certPath = '';
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +53,8 @@ class SyncService {
|
|||
|
||||
// 2. query changes
|
||||
try {
|
||||
$response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
|
||||
$absoluteUri = $this->prepareUri($url, $addressBookUrl);
|
||||
$response = $this->requestSyncReport($absoluteUri, $userName, $sharedSecret, $syncToken);
|
||||
} catch (ClientExceptionInterface $ex) {
|
||||
if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
|
||||
// remote server revoked access to the address book, remove it
|
||||
|
|
@ -71,7 +71,8 @@ class SyncService {
|
|||
foreach ($response['response'] as $resource => $status) {
|
||||
$cardUri = basename($resource);
|
||||
if (isset($status[200])) {
|
||||
$vCard = $this->download($url, $userName, $sharedSecret, $resource);
|
||||
$absoluteUrl = $this->prepareUri($url, $resource);
|
||||
$vCard = $this->download($absoluteUrl, $userName, $sharedSecret);
|
||||
$this->atomic(function () use ($addressBookId, $cardUri, $vCard): void {
|
||||
$existingCard = $this->backend->getCard($addressBookId, $cardUri);
|
||||
if ($existingCard === false) {
|
||||
|
|
@ -130,148 +131,6 @@ class SyncService {
|
|||
]);
|
||||
}
|
||||
|
||||
private function prepareUri(string $host, string $path): string {
|
||||
/*
|
||||
* The trailing slash is important for merging the uris.
|
||||
*
|
||||
* $host is stored in oc_trusted_servers.url and usually without a trailing slash.
|
||||
*
|
||||
* Example for a report request
|
||||
*
|
||||
* $host = 'https://server.internal/cloud'
|
||||
* $path = 'remote.php/dav/addressbooks/system/system/system'
|
||||
*
|
||||
* Without the trailing slash, the webroot is missing:
|
||||
* https://server.internal/remote.php/dav/addressbooks/system/system/system
|
||||
*
|
||||
* Example for a download request
|
||||
*
|
||||
* $host = 'https://server.internal/cloud'
|
||||
* $path = '/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf'
|
||||
*
|
||||
* The response from the remote usually contains the webroot already and must be normalized to:
|
||||
* https://server.internal/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf
|
||||
*/
|
||||
$host = rtrim($host, '/') . '/';
|
||||
|
||||
$uri = \GuzzleHttp\Psr7\UriResolver::resolve(
|
||||
\GuzzleHttp\Psr7\Utils::uriFor($host),
|
||||
\GuzzleHttp\Psr7\Utils::uriFor($path)
|
||||
);
|
||||
|
||||
return (string)$uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws ParseException
|
||||
*/
|
||||
protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
|
||||
$client = $this->clientService->newClient();
|
||||
$uri = $this->prepareUri($url, $addressBookUrl);
|
||||
|
||||
$options = [
|
||||
'auth' => [$userName, $sharedSecret],
|
||||
'body' => $this->buildSyncCollectionRequestBody($syncToken),
|
||||
'headers' => ['Content-Type' => 'application/xml'],
|
||||
'timeout' => $this->config->getSystemValueInt('carddav_sync_request_timeout', IClient::DEFAULT_REQUEST_TIMEOUT),
|
||||
'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
|
||||
];
|
||||
|
||||
$response = $client->request(
|
||||
'REPORT',
|
||||
$uri,
|
||||
$options
|
||||
);
|
||||
|
||||
$body = $response->getBody();
|
||||
assert(is_string($body));
|
||||
|
||||
return $this->parseMultiStatus($body, $addressBookUrl);
|
||||
}
|
||||
|
||||
protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): string {
|
||||
$client = $this->clientService->newClient();
|
||||
$uri = $this->prepareUri($url, $resourcePath);
|
||||
|
||||
$options = [
|
||||
'auth' => [$userName, $sharedSecret],
|
||||
'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
|
||||
];
|
||||
|
||||
$response = $client->get(
|
||||
$uri,
|
||||
$options
|
||||
);
|
||||
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
private function buildSyncCollectionRequestBody(?string $syncToken): string {
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->formatOutput = true;
|
||||
$root = $dom->createElementNS('DAV:', 'd:sync-collection');
|
||||
$sync = $dom->createElement('d:sync-token', $syncToken ?? '');
|
||||
$prop = $dom->createElement('d:prop');
|
||||
$cont = $dom->createElement('d:getcontenttype');
|
||||
$etag = $dom->createElement('d:getetag');
|
||||
|
||||
$prop->appendChild($cont);
|
||||
$prop->appendChild($etag);
|
||||
$root->appendChild($sync);
|
||||
$root->appendChild($prop);
|
||||
$dom->appendChild($root);
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
|
||||
* @throws ParseException
|
||||
*/
|
||||
private function parseMultiStatus(string $body, string $addressBookUrl): array {
|
||||
/** @var MultiStatus $multiStatus */
|
||||
$multiStatus = (new Service())->expect('{DAV:}multistatus', $body);
|
||||
|
||||
$result = [];
|
||||
$truncated = false;
|
||||
|
||||
foreach ($multiStatus->getResponses() as $response) {
|
||||
$href = $response->getHref();
|
||||
if ($response->getHttpStatus() === '507' && $this->isResponseForRequestUri($href, $addressBookUrl)) {
|
||||
$truncated = true;
|
||||
} else {
|
||||
$result[$response->getHref()] = $response->getResponseProperties();
|
||||
}
|
||||
}
|
||||
|
||||
return ['response' => $result, 'token' => $multiStatus->getSyncToken(), 'truncated' => $truncated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the provided response URI corresponds to the given request URI.
|
||||
*/
|
||||
private function isResponseForRequestUri(string $responseUri, string $requestUri): bool {
|
||||
/*
|
||||
* Example response uri:
|
||||
*
|
||||
* /remote.php/dav/addressbooks/system/system/system/
|
||||
* /cloud/remote.php/dav/addressbooks/system/system/system/ (when installed in a subdirectory)
|
||||
*
|
||||
* Example request uri:
|
||||
*
|
||||
* remote.php/dav/addressbooks/system/system/system
|
||||
*
|
||||
* References:
|
||||
* https://github.com/nextcloud/3rdparty/blob/e0a509739b13820f0a62ff9cad5d0fede00e76ee/sabre/dav/lib/DAV/Sync/Plugin.php#L172-L174
|
||||
* https://github.com/nextcloud/server/blob/b40acb34a39592070d8455eb91c5364c07928c50/apps/federation/lib/SyncFederationAddressBooks.php#L41
|
||||
*/
|
||||
return str_ends_with(
|
||||
rtrim($responseUri, '/'),
|
||||
rtrim($requestUri, '/')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IUser $user
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
129
apps/dav/lib/DAV/RemoteUserPrincipalBackend.php
Normal file
129
apps/dav/lib/DAV/RemoteUserPrincipalBackend.php
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\DAV;
|
||||
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
|
||||
|
||||
class RemoteUserPrincipalBackend implements BackendInterface {
|
||||
public const PRINCIPAL_PREFIX = 'principals/remote-users';
|
||||
|
||||
private bool $hasCachedAllChildren = false;
|
||||
|
||||
/** @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) {
|
||||
if ($prefixPath !== self::PRINCIPAL_PREFIX) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$this->hasCachedAllChildren) {
|
||||
$this->loadChildren();
|
||||
$this->hasCachedAllChildren = true;
|
||||
}
|
||||
|
||||
return $this->principals;
|
||||
}
|
||||
|
||||
public function getPrincipalByPath($path) {
|
||||
[$prefix] = \Sabre\Uri\split($path);
|
||||
if ($prefix !== self::PRINCIPAL_PREFIX) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($this->principalsByPath[$path])) {
|
||||
return $this->principalsByPath[$path];
|
||||
}
|
||||
|
||||
try {
|
||||
$principal = $this->principalUriToPrincipal($path);
|
||||
} catch (\Exception $e) {
|
||||
$principal = null;
|
||||
}
|
||||
$this->principalsByPath[$path] = $principal;
|
||||
return $principal;
|
||||
}
|
||||
|
||||
public function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) {
|
||||
throw new \Sabre\DAV\Exception('Updating remote user principal is not supported');
|
||||
}
|
||||
|
||||
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
|
||||
// Searching is not supported
|
||||
return [];
|
||||
}
|
||||
|
||||
public function findByUri($uri, $principalPrefix) {
|
||||
if (str_starts_with($uri, 'principal:')) {
|
||||
$principal = substr($uri, strlen('principal:'));
|
||||
$principal = $this->getPrincipalByPath($principal);
|
||||
if ($principal !== null) {
|
||||
return $principal['uri'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getGroupMemberSet($principal) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getGroupMembership($principal) {
|
||||
// TODO: for now the group principal has only one member, the user itself
|
||||
$principal = $this->getPrincipalByPath($principal);
|
||||
if (!$principal) {
|
||||
throw new \Sabre\DAV\Exception('Principal not found');
|
||||
}
|
||||
|
||||
return [$principal['uri']];
|
||||
}
|
||||
|
||||
public function setGroupMemberSet($principal, array $members) {
|
||||
throw new \Sabre\DAV\Exception('Adding members to remote user is not supported');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{'{DAV:}displayname': string, '{http://nextcloud.com/ns}cloud-id': \OCP\Federation\ICloudId, uri: string}
|
||||
*/
|
||||
private function principalUriToPrincipal(string $principalUri): array {
|
||||
[, $name] = \Sabre\Uri\split($principalUri);
|
||||
$cloudId = $this->cloudIdManager->resolveCloudId(base64_decode($name));
|
||||
return [
|
||||
'uri' => $principalUri,
|
||||
'{DAV:}displayname' => $cloudId->getDisplayId(),
|
||||
'{http://nextcloud.com/ns}cloud-id' => $cloudId,
|
||||
];
|
||||
}
|
||||
|
||||
private function loadChildren(): void {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\DAV\DAV\Sharing;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederationSharingService;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCP\AppFramework\Db\TTransactional;
|
||||
use OCP\ICache;
|
||||
use OCP\ICacheFactory;
|
||||
|
|
@ -31,8 +33,13 @@ abstract class Backend {
|
|||
private IUserManager $userManager,
|
||||
private IGroupManager $groupManager,
|
||||
private Principal $principalBackend,
|
||||
private RemoteUserPrincipalBackend $remoteUserPrincipalBackend,
|
||||
private ICacheFactory $cacheFactory,
|
||||
private SharingService $service,
|
||||
// TODO: Make `FederationSharingService` abstract once we support federated address book
|
||||
// sharing. The abstract sharing backend should not take a service scoped to calendars
|
||||
// by default.
|
||||
private FederationSharingService $federationSharingService,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
$this->shareCache = $this->cacheFactory->createInMemory();
|
||||
|
|
@ -45,7 +52,9 @@ abstract class Backend {
|
|||
public function updateShares(IShareable $shareable, array $add, array $remove, array $oldShares = []): void {
|
||||
$this->shareCache->clear();
|
||||
foreach ($add as $element) {
|
||||
$principal = $this->principalBackend->findByUri($element['href'], '');
|
||||
// Hacky code below ... shouldn't we check the whole (principal) root collection instead?
|
||||
$principal = $this->principalBackend->findByUri($element['href'], '')
|
||||
?? $this->remoteUserPrincipalBackend->findByUri($element['href'], '');
|
||||
if (empty($principal)) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -53,7 +62,7 @@ abstract class Backend {
|
|||
// We need to validate manually because some principals are only virtual
|
||||
// i.e. Group principals
|
||||
$principalparts = explode('/', $principal, 3);
|
||||
if (count($principalparts) !== 3 || $principalparts[0] !== 'principals' || !in_array($principalparts[1], ['users', 'groups', 'circles'], true)) {
|
||||
if (count($principalparts) !== 3 || $principalparts[0] !== 'principals' || !in_array($principalparts[1], ['users', 'groups', 'circles', 'remote-users'], true)) {
|
||||
// Invalid principal
|
||||
continue;
|
||||
}
|
||||
|
|
@ -75,10 +84,16 @@ abstract class Backend {
|
|||
$access = $element['readOnly'] ? Backend::ACCESS_READ : Backend::ACCESS_READ_WRITE;
|
||||
}
|
||||
|
||||
$this->service->shareWith($shareable->getResourceId(), $principal, $access);
|
||||
if ($principalparts[1] === 'remote-users') {
|
||||
$this->federationSharingService->shareWith($shareable, $principal, $access);
|
||||
} else {
|
||||
$this->service->shareWith($shareable->getResourceId(), $principal, $access);
|
||||
}
|
||||
}
|
||||
foreach ($remove as $element) {
|
||||
$principal = $this->principalBackend->findByUri($element, '');
|
||||
// Hacky code below ... shouldn't we check the whole (principal) root collection instead?
|
||||
$principal = $this->principalBackend->findByUri($element, '')
|
||||
?? $this->remoteUserPrincipalBackend->findByUri($element, '');
|
||||
if (empty($principal)) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -124,14 +139,14 @@ abstract class Backend {
|
|||
$rows = $this->service->getShares($resourceId);
|
||||
$shares = [];
|
||||
foreach ($rows as $row) {
|
||||
$p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
|
||||
$p = $this->getPrincipalByPath($row['principaluri']);
|
||||
$shares[] = [
|
||||
'href' => "principal:{$row['principaluri']}",
|
||||
'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '',
|
||||
'status' => 1,
|
||||
'readOnly' => (int)$row['access'] === Backend::ACCESS_READ,
|
||||
'{http://owncloud.org/ns}principal' => (string)$row['principaluri'],
|
||||
'{http://owncloud.org/ns}group-share' => isset($p['uri']) && (str_starts_with($p['uri'], 'principals/groups') || str_starts_with($p['uri'], 'principals/circles'))
|
||||
'{http://owncloud.org/ns}group-share' => isset($p['uri']) && (str_starts_with($p['uri'], 'principals/groups') || str_starts_with($p['uri'], 'principals/circles')),
|
||||
];
|
||||
}
|
||||
$this->shareCache->set((string)$resourceId, $shares);
|
||||
|
|
@ -150,7 +165,7 @@ abstract class Backend {
|
|||
$sharesByResource = array_fill_keys($resourceIds, []);
|
||||
foreach ($rows as $row) {
|
||||
$resourceId = (int)$row['resourceid'];
|
||||
$p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
|
||||
$p = $this->getPrincipalByPath($row['principaluri']);
|
||||
$sharesByResource[$resourceId][] = [
|
||||
'href' => "principal:{$row['principaluri']}",
|
||||
'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '',
|
||||
|
|
@ -237,4 +252,17 @@ abstract class Backend {
|
|||
|
||||
return count(array_intersect($memberships, $shares)) > 0;
|
||||
}
|
||||
|
||||
public function getSharesByShareePrincipal(string $principal): array {
|
||||
return $this->service->getSharesByPrincipals([$principal]);
|
||||
}
|
||||
|
||||
private function getPrincipalByPath(string $principalUri): ?array {
|
||||
// Hacky code below ... shouldn't we check the whole (principal) root collection instead?
|
||||
if (str_starts_with($principalUri, RemoteUserPrincipalBackend::PRINCIPAL_PREFIX)) {
|
||||
return $this->remoteUserPrincipalBackend->getPrincipalByPath($principalUri);
|
||||
}
|
||||
|
||||
return $this->principalBackend->getPrincipalByPath($principalUri);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,19 @@ class SharingMapper {
|
|||
$query->executeStatement();
|
||||
}
|
||||
|
||||
public function shareWithToken(int $resourceId, string $resourceType, int $access, string $principal, string $token): void {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->insert('dav_shares')
|
||||
->values([
|
||||
'principaluri' => $query->createNamedParameter($principal),
|
||||
'type' => $query->createNamedParameter($resourceType),
|
||||
'access' => $query->createNamedParameter($access),
|
||||
'resourceid' => $query->createNamedParameter($resourceId),
|
||||
'token' => $query->createNamedParameter($token),
|
||||
]);
|
||||
$query->executeStatement();
|
||||
}
|
||||
|
||||
public function deleteShare(int $resourceId, string $resourceType, string $principal): void {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->delete('dav_shares');
|
||||
|
|
@ -134,4 +147,103 @@ class SharingMapper {
|
|||
->andWhere($query->expr()->eq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)))
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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("$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();
|
||||
$result->closeCursor();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return array{uri: string, principaluri: string}[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function getSharedCalendarsForRemoteUser(
|
||||
string $remoteUserPrincipalUri,
|
||||
string $token,
|
||||
): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('c.uri', 'c.principaluri')
|
||||
->from('dav_shares', 'ds')
|
||||
->join('ds', 'calendars', 'c', $qb->expr()->eq(
|
||||
'ds.resourceid',
|
||||
'c.id',
|
||||
IQueryBuilder::PARAM_INT,
|
||||
))
|
||||
->where($qb->expr()->eq(
|
||||
'ds.type',
|
||||
$qb->createNamedParameter('calendar', IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
))
|
||||
->andWhere($qb->expr()->eq(
|
||||
'ds.principaluri',
|
||||
$qb->createNamedParameter($remoteUserPrincipalUri, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
))
|
||||
->andWhere($qb->expr()->eq(
|
||||
'ds.token',
|
||||
$qb->createNamedParameter($token, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
));
|
||||
$result = $qb->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $principalUris
|
||||
*
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function getSharesByPrincipalsAndResource(
|
||||
array $principalUris,
|
||||
int $resourceId,
|
||||
string $resourceType,
|
||||
): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('dav_shares')
|
||||
->where($qb->expr()->in(
|
||||
'principaluri',
|
||||
$qb->createNamedParameter($principalUris, IQueryBuilder::PARAM_STR_ARRAY),
|
||||
IQueryBuilder::PARAM_STR_ARRAY,
|
||||
))
|
||||
->andWhere($qb->expr()->eq(
|
||||
'resourceid',
|
||||
$qb->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT),
|
||||
IQueryBuilder::PARAM_INT,
|
||||
))
|
||||
->andWhere($qb->expr()->eq(
|
||||
'type',
|
||||
$qb->createNamedParameter($resourceType, IQueryBuilder::PARAM_STR),
|
||||
IQueryBuilder::PARAM_STR,
|
||||
));
|
||||
$result = $qb->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,4 +50,11 @@ abstract class SharingService {
|
|||
public function getSharesForIds(array $resourceIds): array {
|
||||
return $this->mapper->getSharesForIds($resourceIds, $this->getResourceType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $principals
|
||||
*/
|
||||
public function getSharesByPrincipals(array $principals): array {
|
||||
return $this->mapper->getSharesByPrincipals($principals, $this->getResourceType());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
105
apps/dav/lib/Listener/CalendarFederationNotificationListener.php
Normal file
105
apps/dav/lib/Listener/CalendarFederationNotificationListener.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Listener;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\CalendarFederationNotifier;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\Calendar\Events\CalendarObjectCreatedEvent;
|
||||
use OCP\Calendar\Events\CalendarObjectDeletedEvent;
|
||||
use OCP\Calendar\Events\CalendarObjectUpdatedEvent;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\OCM\Exceptions\OCMProviderException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener<Event|CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectDeletedEvent>
|
||||
*/
|
||||
class CalendarFederationNotificationListener implements IEventListener {
|
||||
public function __construct(
|
||||
private readonly ICloudIdManager $cloudIdManager,
|
||||
private readonly CalendarFederationNotifier $calendarFederationNotifier,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly SharingMapper $sharingMapper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof CalendarObjectCreatedEvent)
|
||||
&& !($event instanceof CalendarObjectUpdatedEvent)
|
||||
&& !($event instanceof CalendarObjectDeletedEvent)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$remoteUserShares = array_filter($event->getShares(), function (array $share): bool {
|
||||
$sharedWithPrincipal = $share['{http://owncloud.org/ns}principal'] ?? '';
|
||||
[$prefix] = \Sabre\Uri\split($sharedWithPrincipal);
|
||||
return $prefix === RemoteUserPrincipalBackend::PRINCIPAL_PREFIX;
|
||||
});
|
||||
if (empty($remoteUserShares)) {
|
||||
// Not shared with any remote user
|
||||
return;
|
||||
}
|
||||
|
||||
$calendarInfo = $event->getCalendarData();
|
||||
$remoteUserPrincipals = array_map(
|
||||
static fn (array $share) => $share['{http://owncloud.org/ns}principal'],
|
||||
$remoteUserShares,
|
||||
);
|
||||
$remoteShares = $this->sharingMapper->getSharesByPrincipalsAndResource(
|
||||
$remoteUserPrincipals,
|
||||
(int)$calendarInfo['id'],
|
||||
'calendar',
|
||||
);
|
||||
|
||||
foreach ($remoteShares as $share) {
|
||||
[, $name] = \Sabre\Uri\split($share['principaluri']);
|
||||
$shareWithRaw = base64_decode($name);
|
||||
try {
|
||||
$shareWith = $this->cloudIdManager->resolveCloudId($shareWithRaw);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Not a valid remote user principal
|
||||
continue;
|
||||
}
|
||||
|
||||
[, $sharedByUid] = \Sabre\Uri\split($calendarInfo['principaluri']);
|
||||
|
||||
$remoteUrl = $shareWith->getRemote();
|
||||
try {
|
||||
$response = $this->calendarFederationNotifier->notifySyncCalendar(
|
||||
$shareWith,
|
||||
$sharedByUid,
|
||||
$calendarInfo['uri'],
|
||||
$share['token'],
|
||||
);
|
||||
} catch (OCMProviderException $e) {
|
||||
$this->logger->error("Failed to send SYNC_CALENDAR notification to remote $remoteUrl", [
|
||||
'exception' => $e,
|
||||
'shareWith' => $shareWith->getId(),
|
||||
'calendarName' => $calendarInfo['uri'],
|
||||
'calendarOwner' => $sharedByUid,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
|
||||
$this->logger->error("Remote $remoteUrl rejected SYNC_CALENDAR notification", [
|
||||
'statusCode' => $response->getStatusCode(),
|
||||
'shareWith' => $shareWith->getId(),
|
||||
'calendarName' => $calendarInfo['uri'],
|
||||
'calendarOwner' => $sharedByUid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
apps/dav/lib/Listener/SabrePluginAuthInitListener.php
Normal file
35
apps/dav/lib/Listener/SabrePluginAuthInitListener.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Listener;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarAuth;
|
||||
use OCA\DAV\Events\SabrePluginAuthInitEvent;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Server;
|
||||
use Sabre\DAV\Auth\Plugin;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener<Event|SabrePluginAuthInitEvent>
|
||||
*/
|
||||
class SabrePluginAuthInitListener implements IEventListener {
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof SabrePluginAuthInitEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$server = $event->getServer();
|
||||
$authPlugin = $server->getPlugin('auth');
|
||||
if ($authPlugin instanceof Plugin) {
|
||||
$authBackend = Server::get(FederatedCalendarAuth::class);
|
||||
$authPlugin->addBackend($authBackend);
|
||||
}
|
||||
}
|
||||
}
|
||||
102
apps/dav/lib/Migration/Version1034Date20250605132605.php
Normal file
102
apps/dav/lib/Migration/Version1034Date20250605132605.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version1034Date20250605132605 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
$davSharesTable = $schema->getTable('dav_shares');
|
||||
if (!$davSharesTable->hasColumn('token')) {
|
||||
$davSharesTable->addColumn('token', Types::STRING, [
|
||||
'notnull' => false,
|
||||
'default' => null,
|
||||
'length' => 255,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$schema->hasTable('calendars_federated')) {
|
||||
$federatedCalendarsTable = $schema->createTable('calendars_federated');
|
||||
$federatedCalendarsTable->addColumn('id', Types::BIGINT, [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('display_name', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('color', Types::STRING, [
|
||||
'notnull' => false,
|
||||
'length' => 7,
|
||||
'default' => null,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('uri', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('principaluri', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('remote_Url', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('token', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('sync_token', Types::INTEGER, [
|
||||
'notnull' => true,
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('last_sync', Types::BIGINT, [
|
||||
'notnull' => false,
|
||||
'unsigned' => true,
|
||||
'default' => null,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('shared_by', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('shared_by_display_name', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('components', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$federatedCalendarsTable->addColumn('permissions', Types::INTEGER, [
|
||||
'notnull' => true,
|
||||
]);
|
||||
$federatedCalendarsTable->setPrimaryKey(['id']);
|
||||
$federatedCalendarsTable->addIndex(['principaluri', 'uri'], 'fedcals_uris_index');
|
||||
$federatedCalendarsTable->addIndex(['last_sync'], 'fedcals_last_sync_index');
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +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\FederatedCalendarFactory;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Principal\Collection;
|
||||
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
|
||||
use OCA\DAV\CalDAV\PublicCalendarRoot;
|
||||
|
|
@ -21,6 +23,7 @@ use OCA\DAV\CardDAV\AddressBookRoot;
|
|||
use OCA\DAV\CardDAV\CardDavBackend;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\GroupPrincipalBackend;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCA\DAV\DAV\SystemPrincipalBackend;
|
||||
use OCA\DAV\Provisioning\Apple\AppleProvisioningNode;
|
||||
use OCA\DAV\SystemTag\SystemTagsByIdCollection;
|
||||
|
|
@ -59,6 +62,7 @@ class RootCollection extends SimpleCollection {
|
|||
$config = Server::get(IConfig::class);
|
||||
$proxyMapper = Server::get(ProxyMapper::class);
|
||||
$rootFolder = Server::get(IRootFolder::class);
|
||||
$federatedCalendarFactory = Server::get(FederatedCalendarFactory::class);
|
||||
|
||||
$userPrincipalBackend = new Principal(
|
||||
$userManager,
|
||||
|
|
@ -76,6 +80,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 = Server::get(RemoteUserPrincipalBackend::class);
|
||||
// as soon as debug mode is enabled we allow listing of principals
|
||||
$disableListing = !$config->getSystemValue('debug', false);
|
||||
|
||||
|
|
@ -88,6 +93,7 @@ class RootCollection extends SimpleCollection {
|
|||
$systemPrincipals->disableListing = $disableListing;
|
||||
$calendarResourcePrincipals = new Collection($calendarResourcePrincipalBackend, 'principals/calendar-resources');
|
||||
$calendarRoomPrincipals = new Collection($calendarRoomPrincipalBackend, 'principals/calendar-rooms');
|
||||
$remoteUserPrincipals = new Collection($remoteUserPrincipalBackend, RemoteUserPrincipalBackend::PRINCIPAL_PREFIX);
|
||||
$calendarSharingBackend = Server::get(Backend::class);
|
||||
|
||||
$filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users');
|
||||
|
|
@ -101,14 +107,18 @@ class RootCollection extends SimpleCollection {
|
|||
$dispatcher,
|
||||
$config,
|
||||
$calendarSharingBackend,
|
||||
Server::get(FederatedCalendarMapper::class),
|
||||
false,
|
||||
);
|
||||
$userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users', $logger);
|
||||
$userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users', $logger, $l10n, $config, $federatedCalendarFactory);
|
||||
$userCalendarRoot->disableListing = $disableListing;
|
||||
|
||||
$resourceCalendarRoot = new CalendarRoot($calendarResourcePrincipalBackend, $caldavBackend, 'principals/calendar-resources', $logger);
|
||||
$remoteUserCalendarRoot = new CalendarRoot($remoteUserPrincipalBackend, $caldavBackend, RemoteUserPrincipalBackend::PRINCIPAL_PREFIX, $logger, $l10n, $config, $federatedCalendarFactory);
|
||||
$remoteUserCalendarRoot->disableListing = $disableListing;
|
||||
|
||||
$resourceCalendarRoot = new CalendarRoot($calendarResourcePrincipalBackend, $caldavBackend, 'principals/calendar-resources', $logger, $l10n, $config, $federatedCalendarFactory);
|
||||
$resourceCalendarRoot->disableListing = $disableListing;
|
||||
$roomCalendarRoot = new CalendarRoot($calendarRoomPrincipalBackend, $caldavBackend, 'principals/calendar-rooms', $logger);
|
||||
$roomCalendarRoot = new CalendarRoot($calendarRoomPrincipalBackend, $caldavBackend, 'principals/calendar-rooms', $logger, $l10n, $config, $federatedCalendarFactory);
|
||||
$roomCalendarRoot->disableListing = $disableListing;
|
||||
|
||||
$publicCalendarRoot = new PublicCalendarRoot($caldavBackend, $l10n, $config, $logger);
|
||||
|
|
@ -179,9 +189,11 @@ class RootCollection extends SimpleCollection {
|
|||
$groupPrincipals,
|
||||
$systemPrincipals,
|
||||
$calendarResourcePrincipals,
|
||||
$calendarRoomPrincipals]),
|
||||
$calendarRoomPrincipals,
|
||||
$remoteUserPrincipals]),
|
||||
$filesCollection,
|
||||
$userCalendarRoot,
|
||||
$remoteUserCalendarRoot,
|
||||
new SimpleCollection('system-calendars', [
|
||||
$resourceCalendarRoot,
|
||||
$roomCalendarRoot,
|
||||
|
|
|
|||
194
apps/dav/lib/Service/ASyncService.php
Normal file
194
apps/dav/lib/Service/ASyncService.php
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Service;
|
||||
|
||||
use OCP\Http\Client\IClient;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use Sabre\DAV\Xml\Response\MultiStatus;
|
||||
use Sabre\DAV\Xml\Service as SabreXmlService;
|
||||
use Sabre\Xml\ParseException;
|
||||
|
||||
/**
|
||||
* Abstract sync service to sync CalDAV and CardDAV data from federated instances.
|
||||
*/
|
||||
abstract class ASyncService {
|
||||
private ?IClient $client = null;
|
||||
|
||||
public function __construct(
|
||||
protected IClientService $clientService,
|
||||
protected IConfig $config,
|
||||
) {
|
||||
}
|
||||
|
||||
private function getClient(): IClient {
|
||||
if ($this->client === null) {
|
||||
$this->client = $this->clientService->newClient();
|
||||
}
|
||||
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
protected function prepareUri(string $host, string $path): string {
|
||||
/*
|
||||
* The trailing slash is important for merging the uris together.
|
||||
*
|
||||
* $host is stored in oc_trusted_servers.url and usually without a trailing slash.
|
||||
*
|
||||
* Example for a report request
|
||||
*
|
||||
* $host = 'https://server.internal/cloud'
|
||||
* $path = 'remote.php/dav/addressbooks/system/system/system'
|
||||
*
|
||||
* Without the trailing slash, the webroot is missing:
|
||||
* https://server.internal/remote.php/dav/addressbooks/system/system/system
|
||||
*
|
||||
* Example for a download request
|
||||
*
|
||||
* $host = 'https://server.internal/cloud'
|
||||
* $path = '/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf'
|
||||
*
|
||||
* The response from the remote usually contains the webroot already and must be normalized to:
|
||||
* https://server.internal/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf
|
||||
*/
|
||||
$host = rtrim($host, '/') . '/';
|
||||
|
||||
$uri = \GuzzleHttp\Psr7\UriResolver::resolve(
|
||||
\GuzzleHttp\Psr7\Utils::uriFor($host),
|
||||
\GuzzleHttp\Psr7\Utils::uriFor($path)
|
||||
);
|
||||
|
||||
return (string)$uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
|
||||
*/
|
||||
protected function requestSyncReport(
|
||||
string $absoluteUrl,
|
||||
string $userName,
|
||||
string $sharedSecret,
|
||||
?string $syncToken,
|
||||
): array {
|
||||
$client = $this->getClient();
|
||||
|
||||
$options = [
|
||||
'auth' => [$userName, $sharedSecret],
|
||||
'body' => $this->buildSyncCollectionRequestBody($syncToken),
|
||||
'headers' => ['Content-Type' => 'application/xml'],
|
||||
'timeout' => $this->config->getSystemValueInt(
|
||||
'carddav_sync_request_timeout',
|
||||
IClient::DEFAULT_REQUEST_TIMEOUT,
|
||||
),
|
||||
'verify' => !$this->config->getSystemValue(
|
||||
'sharing.federation.allowSelfSignedCertificates',
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
$response = $client->request(
|
||||
'REPORT',
|
||||
$absoluteUrl,
|
||||
$options,
|
||||
);
|
||||
|
||||
$body = $response->getBody();
|
||||
assert(is_string($body));
|
||||
|
||||
return $this->parseMultiStatus($body, $absoluteUrl);
|
||||
}
|
||||
|
||||
protected function download(
|
||||
string $absoluteUrl,
|
||||
string $userName,
|
||||
string $sharedSecret,
|
||||
): string {
|
||||
$client = $this->getClient();
|
||||
|
||||
$options = [
|
||||
'auth' => [$userName, $sharedSecret],
|
||||
'verify' => !$this->config->getSystemValue(
|
||||
'sharing.federation.allowSelfSignedCertificates',
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
$response = $client->get(
|
||||
$absoluteUrl,
|
||||
$options,
|
||||
);
|
||||
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
private function buildSyncCollectionRequestBody(?string $syncToken): string {
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->formatOutput = true;
|
||||
$root = $dom->createElementNS('DAV:', 'd:sync-collection');
|
||||
$sync = $dom->createElement('d:sync-token', $syncToken ?? '');
|
||||
$prop = $dom->createElement('d:prop');
|
||||
$cont = $dom->createElement('d:getcontenttype');
|
||||
$etag = $dom->createElement('d:getetag');
|
||||
|
||||
$prop->appendChild($cont);
|
||||
$prop->appendChild($etag);
|
||||
$root->appendChild($sync);
|
||||
$root->appendChild($prop);
|
||||
$dom->appendChild($root);
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
|
||||
* @throws ParseException
|
||||
*/
|
||||
private function parseMultiStatus(string $body, string $resourceUrl): array {
|
||||
/** @var MultiStatus $multiStatus */
|
||||
$multiStatus = (new SabreXmlService())->expect('{DAV:}multistatus', $body);
|
||||
|
||||
$result = [];
|
||||
$truncated = false;
|
||||
|
||||
foreach ($multiStatus->getResponses() as $response) {
|
||||
$href = $response->getHref();
|
||||
if ($response->getHttpStatus() === '507' && $this->isResponseForRequestUri($href, $resourceUrl)) {
|
||||
$truncated = true;
|
||||
} else {
|
||||
$result[$response->getHref()] = $response->getResponseProperties();
|
||||
}
|
||||
}
|
||||
|
||||
return ['response' => $result, 'token' => $multiStatus->getSyncToken(), 'truncated' => $truncated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the provided response URI corresponds to the given request URI.
|
||||
*/
|
||||
private function isResponseForRequestUri(string $responseUri, string $requestUri): bool {
|
||||
/*
|
||||
* Example response uri:
|
||||
*
|
||||
* /remote.php/dav/addressbooks/system/system/system/
|
||||
* /cloud/remote.php/dav/addressbooks/system/system/system/ (when installed in a subdirectory)
|
||||
*
|
||||
* Example request uri:
|
||||
*
|
||||
* https://foo.bar/remote.php/dav/addressbooks/system/system/system
|
||||
*
|
||||
* References:
|
||||
* https://github.com/nextcloud/3rdparty/blob/e0a509739b13820f0a62ff9cad5d0fede00e76ee/sabre/dav/lib/DAV/Sync/Plugin.php#L172-L174
|
||||
* https://github.com/nextcloud/server/blob/b40acb34a39592070d8455eb91c5364c07928c50/apps/federation/lib/SyncFederationAddressBooks.php#L41
|
||||
*/
|
||||
return str_ends_with(
|
||||
rtrim($requestUri, '/'),
|
||||
rtrim($responseUri, '/'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,10 @@ namespace OCA\DAV\Tests\integration\DAV\Sharing;
|
|||
|
||||
use OC\Memcache\NullCache;
|
||||
use OCA\DAV\CalDAV\Calendar;
|
||||
use OCA\DAV\CalDAV\Federation\FederationSharingService;
|
||||
use OCA\DAV\CalDAV\Sharing\Service;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCA\DAV\DAV\Sharing\Backend;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCA\DAV\DAV\Sharing\SharingService;
|
||||
|
|
@ -22,6 +24,7 @@ use OCP\IDBConnection;
|
|||
use OCP\IGroupManager;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Server;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
|
|
@ -39,6 +42,8 @@ class CalDavSharingBackendTest extends TestCase {
|
|||
private SharingMapper $sharingMapper;
|
||||
private SharingService $sharingService;
|
||||
private Backend $sharingBackend;
|
||||
private RemoteUserPrincipalBackend&MockObject $remoteUserPrincipalBackend;
|
||||
private FederationSharingService&MockObject $federationSharingService;
|
||||
|
||||
private $resourceIds = [10001];
|
||||
|
||||
|
|
@ -54,6 +59,8 @@ class CalDavSharingBackendTest extends TestCase {
|
|||
$this->cacheFactory->method('createInMemory')
|
||||
->willReturn(new NullCache());
|
||||
$this->logger = new \Psr\Log\NullLogger();
|
||||
$this->remoteUserPrincipalBackend = $this->createMock(RemoteUserPrincipalBackend::class);
|
||||
$this->federationSharingService = $this->createMock(FederationSharingService::class);
|
||||
|
||||
$this->sharingMapper = new SharingMapper($this->db);
|
||||
$this->sharingService = new Service($this->sharingMapper);
|
||||
|
|
@ -62,8 +69,10 @@ class CalDavSharingBackendTest extends TestCase {
|
|||
$this->userManager,
|
||||
$this->groupManager,
|
||||
$this->principalBackend,
|
||||
$this->remoteUserPrincipalBackend,
|
||||
$this->cacheFactory,
|
||||
$this->sharingService,
|
||||
$this->federationSharingService,
|
||||
$this->logger
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ use OCA\DAV\AppInfo\PluginManager;
|
|||
use OCA\DAV\CalDAV\CachedSubscription;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\CalendarHome;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendar;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarFactory;
|
||||
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
|
||||
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
|
||||
use OCA\DAV\CalDAV\Outbox;
|
||||
|
|
@ -20,6 +22,7 @@ use PHPUnit\Framework\MockObject\MockObject;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\CalDAV\Schedule\Inbox;
|
||||
use Sabre\CalDAV\Subscriptions\Subscription;
|
||||
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
|
||||
use Sabre\DAV\MkCol;
|
||||
use Test\TestCase;
|
||||
|
||||
|
|
@ -28,6 +31,7 @@ class CalendarHomeTest extends TestCase {
|
|||
private array $principalInfo = [];
|
||||
private PluginManager&MockObject $pluginManager;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private FederatedCalendarFactory&MockObject $federatedCalendarFactory;
|
||||
private CalendarHome $calendarHome;
|
||||
|
||||
protected function setUp(): void {
|
||||
|
|
@ -39,11 +43,13 @@ class CalendarHomeTest extends TestCase {
|
|||
];
|
||||
$this->pluginManager = $this->createMock(PluginManager::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->federatedCalendarFactory = $this->createMock(FederatedCalendarFactory::class);
|
||||
|
||||
$this->calendarHome = new CalendarHome(
|
||||
$this->backend,
|
||||
$this->principalInfo,
|
||||
$this->logger,
|
||||
$this->federatedCalendarFactory,
|
||||
false
|
||||
);
|
||||
|
||||
|
|
@ -97,6 +103,12 @@ class CalendarHomeTest extends TestCase {
|
|||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getFederatedCalendarsForUser')
|
||||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
|
|
@ -148,6 +160,10 @@ class CalendarHomeTest extends TestCase {
|
|||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::never())
|
||||
->method('getFederatedCalendarsForUser');
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
|
|
@ -177,6 +193,10 @@ class CalendarHomeTest extends TestCase {
|
|||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::never())
|
||||
->method('getFederatedCalendarsForUser');
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
|
|
@ -232,6 +252,12 @@ class CalendarHomeTest extends TestCase {
|
|||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getFederatedCalendarsForUser')
|
||||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
|
|
@ -268,6 +294,7 @@ class CalendarHomeTest extends TestCase {
|
|||
$this->backend,
|
||||
$this->principalInfo,
|
||||
$this->logger,
|
||||
$this->federatedCalendarFactory,
|
||||
false
|
||||
);
|
||||
|
||||
|
|
@ -292,6 +319,12 @@ class CalendarHomeTest extends TestCase {
|
|||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getFederatedCalendarsForUser')
|
||||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
|
|
@ -328,6 +361,7 @@ class CalendarHomeTest extends TestCase {
|
|||
$this->backend,
|
||||
$this->principalInfo,
|
||||
$this->logger,
|
||||
$this->federatedCalendarFactory,
|
||||
true
|
||||
);
|
||||
|
||||
|
|
@ -344,4 +378,56 @@ class CalendarHomeTest extends TestCase {
|
|||
$this->assertInstanceOf(CachedSubscription::class, $actual[3]);
|
||||
$this->assertInstanceOf(CachedSubscription::class, $actual[4]);
|
||||
}
|
||||
|
||||
public function testGetChildrenFederatedCalendars(): void {
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getCalendarsForUser')
|
||||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getFederatedCalendarsForUser')
|
||||
->with('user-principal-123')
|
||||
->willReturn([
|
||||
[
|
||||
'id' => 10,
|
||||
'uri' => 'fed-cal-1',
|
||||
'principaluri' => 'user-principal-123',
|
||||
'{DAV:}displayname' => 'Federated calendar 1',
|
||||
'{http://sabredav.org/ns}sync-token' => 3,
|
||||
'{http://calendarserver.org/ns/}getctag' => 'http://sabre.io/ns/sync/3',
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']),
|
||||
'{http://owncloud.org/ns}owner-principal' => 'principals/remote-users/c2hhcmVyQGhvc3QudGxkCg==',
|
||||
'{http://owncloud.org/ns}read-only' => 1
|
||||
],
|
||||
[
|
||||
'id' => 11,
|
||||
'uri' => 'fed-cal-2',
|
||||
'principaluri' => 'user-principal-123',
|
||||
'{DAV:}displayname' => 'Federated calendar 2',
|
||||
'{http://sabredav.org/ns}sync-token' => 5,
|
||||
'{http://calendarserver.org/ns/}getctag' => 'http://sabre.io/ns/sync/5',
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']),
|
||||
'{http://owncloud.org/ns}owner-principal' => 'principals/remote-users/c2hhcmVyQGhvc3QudGxkCg==',
|
||||
'{http://owncloud.org/ns}read-only' => 1
|
||||
],
|
||||
]);
|
||||
|
||||
$this->backend
|
||||
->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
->with('user-principal-123')
|
||||
->willReturn([]);
|
||||
|
||||
$actual = $this->calendarHome->getChildren();
|
||||
|
||||
$this->assertCount(5, $actual);
|
||||
$this->assertInstanceOf(Inbox::class, $actual[0]);
|
||||
$this->assertInstanceOf(Outbox::class, $actual[1]);
|
||||
$this->assertInstanceOf(TrashbinHome::class, $actual[2]);
|
||||
$this->assertInstanceOf(FederatedCalendar::class, $actual[3]);
|
||||
$this->assertInstanceOf(FederatedCalendar::class, $actual[4]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\CalendarFederationConfig;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class CalendarFederationConfigTest extends TestCase {
|
||||
private CalendarFederationConfig $config;
|
||||
|
||||
private IAppConfig&MockObject $appConfig;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->appConfig = $this->createMock(IAppConfig::class);
|
||||
|
||||
$this->config = new CalendarFederationConfig(
|
||||
$this->appConfig,
|
||||
);
|
||||
}
|
||||
|
||||
public static function provideIsFederationEnabledData(): array {
|
||||
return [
|
||||
[true],
|
||||
[false],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideIsFederationEnabledData')]
|
||||
public function testIsFederationEnabled(bool $configValue): void {
|
||||
$this->appConfig->expects(self::once())
|
||||
->method('getAppValueBool')
|
||||
->with('enableCalendarFederation', true)
|
||||
->willReturn($configValue);
|
||||
|
||||
$this->assertEquals($configValue, $this->config->isFederationEnabled());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\CalendarFederationNotifier;
|
||||
use OCP\Federation\ICloudFederationFactory;
|
||||
use OCP\Federation\ICloudFederationNotification;
|
||||
use OCP\Federation\ICloudFederationProviderManager;
|
||||
use OCP\Federation\ICloudId;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\OCM\Exceptions\OCMProviderException;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class CalendarFederationNotifierTest extends TestCase {
|
||||
private readonly CalendarFederationNotifier $calendarFederationNotifier;
|
||||
|
||||
private readonly ICloudFederationFactory&MockObject $federationFactory;
|
||||
private readonly ICloudFederationProviderManager&MockObject $federationManager;
|
||||
private readonly IURLGenerator&MockObject $url;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->federationFactory = $this->createMock(ICloudFederationFactory::class);
|
||||
$this->federationManager = $this->createMock(ICloudFederationProviderManager::class);
|
||||
$this->url = $this->createMock(IURLGenerator::class);
|
||||
|
||||
$this->calendarFederationNotifier = new CalendarFederationNotifier(
|
||||
$this->federationFactory,
|
||||
$this->federationManager,
|
||||
$this->url,
|
||||
);
|
||||
}
|
||||
|
||||
public function testNotifySyncCalendar(): void {
|
||||
$cloudId = $this->createMock(ICloudId::class);
|
||||
$cloudId->method('getId')
|
||||
->willReturn('remote1@nextcloud.remote');
|
||||
$cloudId->method('getRemote')
|
||||
->willReturn('nextcloud.remote');
|
||||
|
||||
$this->url->expects(self::once())
|
||||
->method('linkTo')
|
||||
->with('', 'remote.php')
|
||||
->willReturn('/remote.php');
|
||||
$this->url->expects(self::once())
|
||||
->method('getAbsoluteURL')
|
||||
->with('/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1')
|
||||
->willReturn('https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1');
|
||||
|
||||
$notification = $this->createMock(ICloudFederationNotification::class);
|
||||
$notification->expects(self::once())
|
||||
->method('setMessage')
|
||||
->with(
|
||||
'SYNC_CALENDAR',
|
||||
'calendar',
|
||||
'calendar',
|
||||
[
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => 'remote1@nextcloud.remote',
|
||||
'calendarUrl' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
],
|
||||
);
|
||||
$this->federationFactory->expects(self::once())
|
||||
->method('getCloudFederationNotification')
|
||||
->willReturn($notification);
|
||||
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$this->federationManager->expects(self::once())
|
||||
->method('sendCloudNotification')
|
||||
->with('nextcloud.remote', $notification)
|
||||
->willReturn($response);
|
||||
|
||||
$this->calendarFederationNotifier->notifySyncCalendar($cloudId, 'host1', 'cal1', 'token');
|
||||
}
|
||||
|
||||
public function testNotifySyncCalendarShouldRethrowOcmException(): void {
|
||||
$cloudId = $this->createMock(ICloudId::class);
|
||||
$cloudId->method('getId')
|
||||
->willReturn('remote1@nextcloud.remote');
|
||||
$cloudId->method('getRemote')
|
||||
->willReturn('nextcloud.remote');
|
||||
|
||||
$this->url->expects(self::once())
|
||||
->method('linkTo')
|
||||
->with('', 'remote.php')
|
||||
->willReturn('/remote.php');
|
||||
$this->url->expects(self::once())
|
||||
->method('getAbsoluteURL')
|
||||
->with('/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1')
|
||||
->willReturn('https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1');
|
||||
|
||||
$notification = $this->createMock(ICloudFederationNotification::class);
|
||||
$notification->expects(self::once())
|
||||
->method('setMessage')
|
||||
->with(
|
||||
'SYNC_CALENDAR',
|
||||
'calendar',
|
||||
'calendar',
|
||||
[
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => 'remote1@nextcloud.remote',
|
||||
'calendarUrl' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
],
|
||||
);
|
||||
$this->federationFactory->expects(self::once())
|
||||
->method('getCloudFederationNotification')
|
||||
->willReturn($notification);
|
||||
|
||||
$this->federationManager->expects(self::once())
|
||||
->method('sendCloudNotification')
|
||||
->with('nextcloud.remote', $notification)
|
||||
->willThrowException(new OCMProviderException('I threw this'));
|
||||
|
||||
$this->expectException(OCMProviderException::class);
|
||||
$this->expectExceptionMessage('I threw this');
|
||||
$this->calendarFederationNotifier->notifySyncCalendar($cloudId, 'host1', 'cal1', 'token');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,484 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OC\BackgroundJob\JobList;
|
||||
use OCA\DAV\BackgroundJob\FederatedCalendarSyncJob;
|
||||
use OCA\DAV\CalDAV\Federation\CalendarFederationConfig;
|
||||
use OCA\DAV\CalDAV\Federation\CalendarFederationProvider;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\Federation\Exceptions\BadRequestException;
|
||||
use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
|
||||
use OCP\Federation\ICloudFederationShare;
|
||||
use OCP\Federation\ICloudId;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Share\Exceptions\ShareNotFound;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class CalendarFederationProviderTest extends TestCase {
|
||||
private CalendarFederationProvider $calendarFederationProvider;
|
||||
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
|
||||
private CalendarFederationConfig&MockObject $calendarFederationConfig;
|
||||
private IJobList&MockObject $jobList;
|
||||
private ICloudIdManager&MockObject $cloudIdManager;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
|
||||
$this->calendarFederationConfig = $this->createMock(CalendarFederationConfig::class);
|
||||
$this->jobList = $this->createMock(JobList::class);
|
||||
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
|
||||
|
||||
$this->calendarFederationProvider = new CalendarFederationProvider(
|
||||
$this->logger,
|
||||
$this->federatedCalendarMapper,
|
||||
$this->calendarFederationConfig,
|
||||
$this->jobList,
|
||||
$this->cloudIdManager,
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetShareType(): void {
|
||||
$this->assertEquals('calendar', $this->calendarFederationProvider->getShareType());
|
||||
}
|
||||
|
||||
public function testGetSupportedShareTypes(): void {
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['user'],
|
||||
$this->calendarFederationProvider->getSupportedShareTypes(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShareReceived(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn([
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$share->method('getShareWith')
|
||||
->willReturn('sharee1');
|
||||
$share->method('getShareSecret')
|
||||
->willReturn('token');
|
||||
$share->method('getSharedBy')
|
||||
->willReturn('user1@nextcloud.remote');
|
||||
$share->method('getSharedByDisplayName')
|
||||
->willReturn('User 1');
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('deleteByUri')
|
||||
->with(
|
||||
'principals/users/sharee1',
|
||||
'ae4b8ab904076fff2b955ea21b1a0d92',
|
||||
);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('insert')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $calendar) {
|
||||
$this->assertEquals('principals/users/sharee1', $calendar->getPrincipaluri());
|
||||
$this->assertEquals('ae4b8ab904076fff2b955ea21b1a0d92', $calendar->getUri());
|
||||
$this->assertEquals('https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1', $calendar->getRemoteUrl());
|
||||
$this->assertEquals('Calendar 1', $calendar->getDisplayName());
|
||||
$this->assertEquals('#ff0000', $calendar->getColor());
|
||||
$this->assertEquals('token', $calendar->getToken());
|
||||
$this->assertEquals('user1@nextcloud.remote', $calendar->getSharedBy());
|
||||
$this->assertEquals('User 1', $calendar->getSharedByDisplayName());
|
||||
$this->assertEquals(1, $calendar->getPermissions());
|
||||
$this->assertEquals('VEVENT,VTODO', $calendar->getComponents());
|
||||
|
||||
$calendar->setId(10);
|
||||
return $calendar;
|
||||
});
|
||||
|
||||
$this->jobList->expects(self::once())
|
||||
->method('add')
|
||||
->with(FederatedCalendarSyncJob::class, ['id' => 10]);
|
||||
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithInvalidProtocolVersion(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn([
|
||||
'version' => 'unknown',
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(ProviderCouldNotAddShareException::class);
|
||||
$this->expectExceptionMessage('Unknown protocol version');
|
||||
$this->expectExceptionCode(400);
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithoutProtocolVersion(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn([
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(ProviderCouldNotAddShareException::class);
|
||||
$this->expectExceptionMessage('Unknown protocol version');
|
||||
$this->expectExceptionCode(400);
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithDisabledConfig(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(false);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(ProviderCouldNotAddShareException::class);
|
||||
$this->expectExceptionMessage('Server does not support calendar federation');
|
||||
$this->expectExceptionCode(503);
|
||||
$this->calendarFederationProvider->shareReceived($share);
|
||||
}
|
||||
|
||||
public function testShareReceivedWithUnsupportedShareType(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('foobar');
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(ProviderCouldNotAddShareException::class);
|
||||
$this->expectExceptionMessage('Support for sharing with non-users not implemented yet');
|
||||
$this->expectExceptionCode(501);
|
||||
$this->calendarFederationProvider->shareReceived($share);
|
||||
}
|
||||
|
||||
public static function provideIncompleteProtocolData(): array {
|
||||
return [
|
||||
[[
|
||||
'version' => 'v1',
|
||||
'url' => '',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]],
|
||||
[[
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => '',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideIncompleteProtocolData')]
|
||||
public function testShareReceivedWithIncompleteProtocolData(array $protocol): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn($protocol);
|
||||
$share->method('getShareWith')
|
||||
->willReturn('sharee1');
|
||||
$share->method('getShareSecret')
|
||||
->willReturn('token');
|
||||
$share->method('getSharedBy')
|
||||
->willReturn('user1@nextcloud.remote');
|
||||
$share->method('getSharedByDisplayName')
|
||||
->willReturn('User 1');
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(ProviderCouldNotAddShareException::class);
|
||||
$this->expectExceptionMessage('Incomplete protocol data');
|
||||
$this->expectExceptionCode(400);
|
||||
$this->calendarFederationProvider->shareReceived($share);
|
||||
}
|
||||
|
||||
public function testShareReceivedWithUnsupportedAccess(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn([
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 2, // Backend::ACCESS_READ_WRITE
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$share->method('getShareWith')
|
||||
->willReturn('sharee1');
|
||||
$share->method('getShareSecret')
|
||||
->willReturn('token');
|
||||
$share->method('getSharedBy')
|
||||
->willReturn('user1@nextcloud.remote');
|
||||
$share->method('getSharedByDisplayName')
|
||||
->willReturn('User 1');
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(ProviderCouldNotAddShareException::class);
|
||||
$this->expectExceptionMessageMatches('/Unsupported access value: [0-9]+/');
|
||||
$this->expectExceptionCode(400);
|
||||
$this->calendarFederationProvider->shareReceived($share);
|
||||
}
|
||||
|
||||
public function testNotificationReceivedWithUnknownNotification(): void {
|
||||
$actual = $this->calendarFederationProvider->notificationReceived('UNKNOWN', 'calendar', [
|
||||
'sharedSecret' => 'token',
|
||||
'foobar' => 'baz',
|
||||
]);
|
||||
$this->assertEquals([], $actual);
|
||||
}
|
||||
|
||||
public function testNotificationReceivedWithInvalidProviderId(): void {
|
||||
$this->expectException(BadRequestException::class);
|
||||
$this->calendarFederationProvider->notificationReceived('SYNC_CALENDAR', 'foobar', [
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => 'remote1@nextcloud.remote',
|
||||
'calendarUrl' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testNotificationReceivedWithSyncCalendarNotification(): void {
|
||||
$cloudId = $this->createMock(ICloudId::class);
|
||||
$cloudId->method('getId')
|
||||
->willReturn('remote1@nextcloud.remote');
|
||||
$cloudId->method('getUser')
|
||||
->willReturn('remote1');
|
||||
$cloudId->method('getRemote')
|
||||
->willReturn('nextcloud.remote');
|
||||
|
||||
$this->cloudIdManager->expects(self::once())
|
||||
->method('resolveCloudId')
|
||||
->with('remote1@nextcloud.remote')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$calendar1 = new FederatedCalendarEntity();
|
||||
$calendar1->setId(10);
|
||||
$calendar2 = new FederatedCalendarEntity();
|
||||
$calendar2->setId(11);
|
||||
$calendars = [
|
||||
$calendar1,
|
||||
$calendar2,
|
||||
];
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('findByRemoteUrl')
|
||||
->with(
|
||||
'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
'principals/users/remote1',
|
||||
'token',
|
||||
)
|
||||
->willReturn($calendars);
|
||||
|
||||
$this->jobList->expects(self::exactly(2))
|
||||
->method('add')
|
||||
->willReturnMap([
|
||||
[FederatedCalendarSyncJob::class, ['id' => 10]],
|
||||
[FederatedCalendarSyncJob::class, ['id' => 11]],
|
||||
]);
|
||||
|
||||
$actual = $this->calendarFederationProvider->notificationReceived(
|
||||
'SYNC_CALENDAR',
|
||||
'calendar',
|
||||
[
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => 'remote1@nextcloud.remote',
|
||||
'calendarUrl' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
],
|
||||
);
|
||||
$this->assertEquals([], $actual);
|
||||
}
|
||||
|
||||
public static function provideIncompleteSyncCalendarNotificationData(): array {
|
||||
return [
|
||||
// Missing shareWith
|
||||
[[
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => '',
|
||||
'calendarUrl' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
]],
|
||||
[[
|
||||
'sharedSecret' => 'token',
|
||||
'calendarUrl' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
]],
|
||||
|
||||
// Missing calendarUrl
|
||||
[[
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => 'remote1@nextcloud.remote',
|
||||
'calendarUrl' => '',
|
||||
]],
|
||||
[[
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => 'remote1@nextcloud.remote',
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideIncompleteSyncCalendarNotificationData')]
|
||||
public function testNotificationReceivedWithSyncCalendarNotificationAndIncompleteData(
|
||||
array $notification,
|
||||
): void {
|
||||
$this->cloudIdManager->expects(self::never())
|
||||
->method('resolveCloudId');
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('findByRemoteUrl');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(BadRequestException::class);
|
||||
$this->calendarFederationProvider->notificationReceived(
|
||||
'SYNC_CALENDAR',
|
||||
'calendar',
|
||||
$notification,
|
||||
);
|
||||
}
|
||||
|
||||
public function testNotificationReceivedWithSyncCalendarNotificationAndInvalidCloudId(): void {
|
||||
$this->cloudIdManager->expects(self::once())
|
||||
->method('resolveCloudId')
|
||||
->with('invalid-cloud-id')
|
||||
->willThrowException(new \InvalidArgumentException());
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('findByRemoteUrl');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(ShareNotFound::class);
|
||||
$this->expectExceptionMessage('Invalid sharee cloud id');
|
||||
$this->calendarFederationProvider->notificationReceived(
|
||||
'SYNC_CALENDAR',
|
||||
'calendar',
|
||||
[
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => 'invalid-cloud-id',
|
||||
'calendarUrl' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function testNotificationReceivedWithSyncCalendarNotificationAndNoCalendars(): void {
|
||||
$cloudId = $this->createMock(ICloudId::class);
|
||||
$cloudId->method('getId')
|
||||
->willReturn('remote1@nextcloud.remote');
|
||||
$cloudId->method('getUser')
|
||||
->willReturn('remote1');
|
||||
$cloudId->method('getRemote')
|
||||
->willReturn('nextcloud.remote');
|
||||
|
||||
$this->cloudIdManager->expects(self::once())
|
||||
->method('resolveCloudId')
|
||||
->with('remote1@nextcloud.remote')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('findByRemoteUrl')
|
||||
->with(
|
||||
'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
'principals/users/remote1',
|
||||
'token',
|
||||
)
|
||||
->willReturn([]);
|
||||
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
$this->expectException(ShareNotFound::class);
|
||||
$this->expectExceptionMessage('Calendar is not shared with the sharee');
|
||||
$this->calendarFederationProvider->notificationReceived(
|
||||
'SYNC_CALENDAR',
|
||||
'calendar',
|
||||
[
|
||||
'sharedSecret' => 'token',
|
||||
'shareWith' => 'remote1@nextcloud.remote',
|
||||
'calendarUrl' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarAuth;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class FederatedCalendarAuthTest extends TestCase {
|
||||
private FederatedCalendarAuth $auth;
|
||||
|
||||
private SharingMapper&MockObject $sharingMapper;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->sharingMapper = $this->createMock(SharingMapper::class);
|
||||
|
||||
$this->auth = new FederatedCalendarAuth(
|
||||
$this->sharingMapper,
|
||||
);
|
||||
}
|
||||
|
||||
private static function encodeBasicAuthHeader(array $userPass): string {
|
||||
return 'Basic ' . base64_encode(implode(':', $userPass));
|
||||
}
|
||||
|
||||
public static function provideCheckData(): array {
|
||||
return [
|
||||
// Valid credentials
|
||||
[
|
||||
'remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
self::encodeBasicAuthHeader(['abcdef123', 'token']),
|
||||
[['uri' => 'cal1', 'principaluri' => 'principals/users/user1']],
|
||||
[true, 'principals/remote-users/abcdef123'],
|
||||
],
|
||||
[
|
||||
'remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
self::encodeBasicAuthHeader(['abcdef123', 'token']),
|
||||
[
|
||||
['uri' => 'other-cal', 'principaluri' => 'principals/users/user1'],
|
||||
['uri' => 'cal1', 'principaluri' => 'principals/users/user1'],
|
||||
],
|
||||
[true, 'principals/remote-users/abcdef123'],
|
||||
],
|
||||
|
||||
// Invalid basic auth header
|
||||
[
|
||||
'remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
self::encodeBasicAuthHeader(['abcdef123']),
|
||||
null,
|
||||
[false, "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured"],
|
||||
],
|
||||
[
|
||||
'remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'Bearer secret-bearer-token',
|
||||
null,
|
||||
[false, "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured"],
|
||||
],
|
||||
[
|
||||
'remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
null,
|
||||
null,
|
||||
[false, "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured"],
|
||||
],
|
||||
|
||||
// Invalid request path
|
||||
[
|
||||
'calendars/user1/cal1',
|
||||
self::encodeBasicAuthHeader(['abcdef123', 'token']),
|
||||
null,
|
||||
[false, 'This request is not for a federated calendar'],
|
||||
],
|
||||
|
||||
// No shared calendars (or invalid credentials)
|
||||
[
|
||||
'remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
self::encodeBasicAuthHeader(['abcdef123', 'token']),
|
||||
[],
|
||||
[false, 'Username or password was incorrect'],
|
||||
],
|
||||
|
||||
// Shared calendar with invalid URI
|
||||
[
|
||||
'remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
self::encodeBasicAuthHeader(['abcdef123', 'token']),
|
||||
[['uri' => 'other-cal', 'principaluri' => 'principals/users/user1']],
|
||||
[false, 'Username or password was incorrect'],
|
||||
],
|
||||
|
||||
// Shared calendar from invalid sharer
|
||||
[
|
||||
'remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
self::encodeBasicAuthHeader(['abcdef123', 'token']),
|
||||
[['uri' => 'cal1', 'principaluri' => 'principals/users/user2']],
|
||||
[false, 'Username or password was incorrect'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideCheckData')]
|
||||
public function testCheck(
|
||||
string $requestPath,
|
||||
?string $authHeader,
|
||||
?array $rows,
|
||||
array $expected,
|
||||
): void {
|
||||
$request = $this->createMock(RequestInterface::class);
|
||||
$request->method('getPath')
|
||||
->willReturn($requestPath);
|
||||
$request->method('getHeader')
|
||||
->with('Authorization')
|
||||
->willReturn($authHeader);
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
|
||||
if ($rows === null) {
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('getSharedCalendarsForRemoteUser');
|
||||
} else {
|
||||
$this->sharingMapper->expects(self::once())
|
||||
->method('getSharedCalendarsForRemoteUser')
|
||||
->with('principals/remote-users/abcdef123', 'token')
|
||||
->willReturn($rows);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $this->auth->check($request, $response));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarImpl;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class FederatedCalendarImplTest extends TestCase {
|
||||
private FederatedCalendarImpl $federatedCalendarImpl;
|
||||
|
||||
private CalDavBackend&MockObject $calDavBackend;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->calDavBackend = $this->createMock(CalDavBackend::class);
|
||||
|
||||
$this->federatedCalendarImpl = new FederatedCalendarImpl(
|
||||
[],
|
||||
$this->calDavBackend,
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetPermissions(): void {
|
||||
$this->assertEquals(1, $this->federatedCalendarImpl->getPermissions());
|
||||
}
|
||||
|
||||
public function testIsDeleted(): void {
|
||||
$this->assertFalse($this->federatedCalendarImpl->isDeleted());
|
||||
}
|
||||
|
||||
public function testIsShared(): void {
|
||||
$this->assertTrue($this->federatedCalendarImpl->isShared());
|
||||
}
|
||||
|
||||
public function testIsWritable(): void {
|
||||
$this->assertFalse($this->federatedCalendarImpl->isWritable());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
|
||||
use OCA\DAV\CalDAV\SyncService as CalDavSyncService;
|
||||
use OCA\DAV\CalDAV\SyncServiceResult;
|
||||
use OCP\Federation\ICloudId;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
private FederatedCalendarSyncService $federatedCalendarSyncService;
|
||||
|
||||
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private CalDavSyncService&MockObject $calDavSyncService;
|
||||
private ICloudIdManager&MockObject $cloudIdManager;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->calDavSyncService = $this->createMock(CalDavSyncService::class);
|
||||
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
|
||||
|
||||
$this->federatedCalendarSyncService = new FederatedCalendarSyncService(
|
||||
$this->federatedCalendarMapper,
|
||||
$this->logger,
|
||||
$this->calDavSyncService,
|
||||
$this->cloudIdManager,
|
||||
);
|
||||
}
|
||||
|
||||
public function testSyncOne(): void {
|
||||
$calendar = new FederatedCalendarEntity();
|
||||
$calendar->setId(1);
|
||||
$calendar->setPrincipaluri('principals/users/user1');
|
||||
$calendar->setRemoteUrl('https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2');
|
||||
$calendar->setSyncToken(100);
|
||||
$calendar->setToken('token');
|
||||
|
||||
$cloudId = $this->createMock(ICloudId::class);
|
||||
$cloudId->method('getId')
|
||||
->willReturn('user1@nextcloud.testing');
|
||||
$this->cloudIdManager->expects(self::once())
|
||||
->method('getCloudId')
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult('http://sabre.io/ns/sync/101', 10));
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('updateSyncTokenAndTime')
|
||||
->with(1, 101);
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTime');
|
||||
|
||||
$this->assertEquals(10, $this->federatedCalendarSyncService->syncOne($calendar));
|
||||
}
|
||||
|
||||
public function testSyncOneUnchanged(): void {
|
||||
$calendar = new FederatedCalendarEntity();
|
||||
$calendar->setId(1);
|
||||
$calendar->setPrincipaluri('principals/users/user1');
|
||||
$calendar->setRemoteUrl('https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2');
|
||||
$calendar->setSyncToken(100);
|
||||
$calendar->setToken('token');
|
||||
|
||||
$cloudId = $this->createMock(ICloudId::class);
|
||||
$cloudId->method('getId')
|
||||
->willReturn('user1@nextcloud.testing');
|
||||
$this->cloudIdManager->expects(self::once())
|
||||
->method('getCloudId')
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult('http://sabre.io/ns/sync/100', 0));
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTokenAndTime');
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('updateSyncTime')
|
||||
->with(1);
|
||||
|
||||
$this->assertEquals(0, $this->federatedCalendarSyncService->syncOne($calendar));
|
||||
}
|
||||
|
||||
public static function provideUnexpectedSyncTokenData(): array {
|
||||
return [
|
||||
['http://sabre.io/ns/sync/'],
|
||||
['http://sabre.io/ns/sync/foobar'],
|
||||
['http://sabre.io/ns/sync/23abc'],
|
||||
['http://nextcloud.com/ns/sync/33'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideUnexpectedSyncTokenData')]
|
||||
public function testSyncOneWithUnexpectedSyncTokenFormat(string $syncToken): void {
|
||||
$calendar = new FederatedCalendarEntity();
|
||||
$calendar->setId(1);
|
||||
$calendar->setPrincipaluri('principals/users/user1');
|
||||
$calendar->setRemoteUrl('https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2');
|
||||
$calendar->setSyncToken(100);
|
||||
$calendar->setToken('token');
|
||||
|
||||
$cloudId = $this->createMock(ICloudId::class);
|
||||
$cloudId->method('getId')
|
||||
->willReturn('user1@nextcloud.testing');
|
||||
$this->cloudIdManager->expects(self::once())
|
||||
->method('getCloudId')
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult($syncToken, 10));
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTokenAndTime');
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTime');
|
||||
|
||||
$this->assertEquals(0, $this->federatedCalendarSyncService->syncOne($calendar));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\Calendar;
|
||||
use OCA\DAV\CalDAV\Federation\FederationSharingService;
|
||||
use OCA\DAV\DAV\Sharing\IShareable;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\Federation\ICloudFederationFactory;
|
||||
use OCP\Federation\ICloudFederationProviderManager;
|
||||
use OCP\Federation\ICloudFederationShare;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\OCM\Exceptions\OCMProviderException;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
|
||||
use Test\TestCase;
|
||||
|
||||
class FederationSharingServiceTest extends TestCase {
|
||||
private FederationSharingService $federationSharingService;
|
||||
|
||||
private readonly ICloudFederationProviderManager&MockObject $federationManager;
|
||||
private readonly ICloudFederationFactory&MockObject $federationFactory;
|
||||
private readonly IUserManager&MockObject $userManager;
|
||||
private readonly IURLGenerator&MockObject $url;
|
||||
private readonly LoggerInterface&MockObject $logger;
|
||||
private readonly ISecureRandom&MockObject $random;
|
||||
private readonly SharingMapper&MockObject $sharingMapper;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->federationManager = $this->createMock(ICloudFederationProviderManager::class);
|
||||
$this->federationFactory = $this->createMock(ICloudFederationFactory::class);
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->url = $this->createMock(IURLGenerator::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->random = $this->createMock(ISecureRandom::class);
|
||||
$this->sharingMapper = $this->createMock(SharingMapper::class);
|
||||
|
||||
$this->federationSharingService = new FederationSharingService(
|
||||
$this->federationManager,
|
||||
$this->federationFactory,
|
||||
$this->userManager,
|
||||
$this->url,
|
||||
$this->logger,
|
||||
$this->random,
|
||||
$this->sharingMapper,
|
||||
);
|
||||
}
|
||||
|
||||
public function testShareWith(): void {
|
||||
$shareable = $this->createMock(Calendar::class);
|
||||
$shareable->method('getOwner')
|
||||
->willReturn('principals/users/host1');
|
||||
$shareable->method('getName')
|
||||
->willReturn('cal1');
|
||||
$shareable->method('getResourceId')
|
||||
->willReturn(10);
|
||||
$shareable->method('getProperties')
|
||||
->willReturnCallback(static fn (array $props) => match ($props[0]) {
|
||||
'{DAV:}displayname' => ['{DAV:}displayname' => 'Calendar 1'],
|
||||
'{http://apple.com/ns/ical/}calendar-color' => [
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#ff0000',
|
||||
],
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => [
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet([
|
||||
'VEVENT',
|
||||
'VTODO',
|
||||
]),
|
||||
]
|
||||
});
|
||||
|
||||
$hostUser = $this->createMock(IUser::class);
|
||||
$hostUser->method('getCloudId')
|
||||
->willReturn('host1@nextcloud.host');
|
||||
$hostUser->method('getDisplayName')
|
||||
->willReturn('Host 1');
|
||||
$hostUser->method('getUID')
|
||||
->willReturn('host1');
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with('host1')
|
||||
->willReturn($hostUser);
|
||||
|
||||
$this->random->expects(self::once())
|
||||
->method('generate')
|
||||
->with(32)
|
||||
->willReturn('token');
|
||||
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->expects(self::once())
|
||||
->method('getProtocol')
|
||||
->willReturn([
|
||||
'preservedValue1' => 'foobar',
|
||||
'preservedValue2' => 'baz',
|
||||
]);
|
||||
$share->expects(self::once())
|
||||
->method('setProtocol')
|
||||
->with([
|
||||
'preservedValue1' => 'foobar',
|
||||
'preservedValue2' => 'baz',
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$this->federationFactory->expects(self::once())
|
||||
->method('getCloudFederationShare')
|
||||
->with(
|
||||
'remote1@nextcloud.remote',
|
||||
'cal1',
|
||||
'Calendar 1',
|
||||
'calendar',
|
||||
'host1@nextcloud.host',
|
||||
'Host 1',
|
||||
'host1@nextcloud.host',
|
||||
'Host 1',
|
||||
'token',
|
||||
'user',
|
||||
'calendar',
|
||||
)
|
||||
->willReturn($share);
|
||||
|
||||
$this->url->expects(self::once())
|
||||
->method('linkTo')
|
||||
->with('', 'remote.php')
|
||||
->willReturn('/remote.php');
|
||||
$this->url->expects(self::once())
|
||||
->method('getAbsoluteURL')
|
||||
->with('/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1')
|
||||
->willReturn('https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1');
|
||||
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getStatusCode')
|
||||
->willReturn(201);
|
||||
$this->federationManager->expects(self::once())
|
||||
->method('sendCloudShare')
|
||||
->with($share)
|
||||
->willReturn($response);
|
||||
|
||||
$this->sharingMapper->expects(self::once())
|
||||
->method('deleteShare')
|
||||
->with(10, 'calendar', 'principals/remote-users/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl');
|
||||
$this->sharingMapper->expects(self::once())
|
||||
->method('shareWithToken')
|
||||
->with(
|
||||
10,
|
||||
'calendar',
|
||||
3,
|
||||
'principals/remote-users/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl',
|
||||
'token',
|
||||
);
|
||||
|
||||
$this->federationSharingService->shareWith(
|
||||
$shareable,
|
||||
'principals/remote-users/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl',
|
||||
3, // Read-only
|
||||
);
|
||||
}
|
||||
|
||||
public function testShareWithWithFailingFederationManager(): void {
|
||||
$shareable = $this->createMock(Calendar::class);
|
||||
$shareable->method('getOwner')
|
||||
->willReturn('principals/users/host1');
|
||||
$shareable->method('getName')
|
||||
->willReturn('cal1');
|
||||
$shareable->method('getResourceId')
|
||||
->willReturn(10);
|
||||
$shareable->method('getProperties')
|
||||
->willReturnCallback(static fn (array $props) => match ($props[0]) {
|
||||
'{DAV:}displayname' => ['{DAV:}displayname' => 'Calendar 1'],
|
||||
'{http://apple.com/ns/ical/}calendar-color' => [
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#ff0000',
|
||||
],
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => [
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet([
|
||||
'VEVENT',
|
||||
'VTODO',
|
||||
]),
|
||||
]
|
||||
});
|
||||
|
||||
$hostUser = $this->createMock(IUser::class);
|
||||
$hostUser->method('getCloudId')
|
||||
->willReturn('host1@nextcloud.host');
|
||||
$hostUser->method('getDisplayName')
|
||||
->willReturn('Host 1');
|
||||
$hostUser->method('getUID')
|
||||
->willReturn('host1');
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with('host1')
|
||||
->willReturn($hostUser);
|
||||
|
||||
$this->random->expects(self::once())
|
||||
->method('generate')
|
||||
->with(32)
|
||||
->willReturn('token');
|
||||
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->expects(self::once())
|
||||
->method('getProtocol')
|
||||
->willReturn([
|
||||
'preservedValue1' => 'foobar',
|
||||
'preservedValue2' => 'baz',
|
||||
]);
|
||||
$share->expects(self::once())
|
||||
->method('setProtocol')
|
||||
->with([
|
||||
'preservedValue1' => 'foobar',
|
||||
'preservedValue2' => 'baz',
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$this->federationFactory->expects(self::once())
|
||||
->method('getCloudFederationShare')
|
||||
->with(
|
||||
'remote1@nextcloud.remote',
|
||||
'cal1',
|
||||
'Calendar 1',
|
||||
'calendar',
|
||||
'host1@nextcloud.host',
|
||||
'Host 1',
|
||||
'host1@nextcloud.host',
|
||||
'Host 1',
|
||||
'token',
|
||||
'user',
|
||||
'calendar',
|
||||
)
|
||||
->willReturn($share);
|
||||
|
||||
$this->url->expects(self::once())
|
||||
->method('linkTo')
|
||||
->with('', 'remote.php')
|
||||
->willReturn('/remote.php');
|
||||
$this->url->expects(self::once())
|
||||
->method('getAbsoluteURL')
|
||||
->with('/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1')
|
||||
->willReturn('https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1');
|
||||
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getStatusCode')
|
||||
->willReturn(201);
|
||||
$this->federationManager->expects(self::once())
|
||||
->method('sendCloudShare')
|
||||
->with($share)
|
||||
->willThrowException(new OCMProviderException());
|
||||
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('deleteShare');
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('shareWithToken');
|
||||
|
||||
$this->federationSharingService->shareWith(
|
||||
$shareable,
|
||||
'principals/remote-users/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl',
|
||||
3, // Read-only
|
||||
);
|
||||
}
|
||||
|
||||
public function testShareWithWithUnsuccessfulResponse(): void {
|
||||
$shareable = $this->createMock(Calendar::class);
|
||||
$shareable->method('getOwner')
|
||||
->willReturn('principals/users/host1');
|
||||
$shareable->method('getName')
|
||||
->willReturn('cal1');
|
||||
$shareable->method('getResourceId')
|
||||
->willReturn(10);
|
||||
$shareable->method('getProperties')
|
||||
->willReturnCallback(static fn (array $props) => match ($props[0]) {
|
||||
'{DAV:}displayname' => ['{DAV:}displayname' => 'Calendar 1'],
|
||||
'{http://apple.com/ns/ical/}calendar-color' => [
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#ff0000',
|
||||
],
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => [
|
||||
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet([
|
||||
'VEVENT',
|
||||
'VTODO',
|
||||
]),
|
||||
]
|
||||
});
|
||||
|
||||
$hostUser = $this->createMock(IUser::class);
|
||||
$hostUser->method('getCloudId')
|
||||
->willReturn('host1@nextcloud.host');
|
||||
$hostUser->method('getDisplayName')
|
||||
->willReturn('Host 1');
|
||||
$hostUser->method('getUID')
|
||||
->willReturn('host1');
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with('host1')
|
||||
->willReturn($hostUser);
|
||||
|
||||
$this->random->expects(self::once())
|
||||
->method('generate')
|
||||
->with(32)
|
||||
->willReturn('token');
|
||||
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->expects(self::once())
|
||||
->method('getProtocol')
|
||||
->willReturn([
|
||||
'preservedValue1' => 'foobar',
|
||||
'preservedValue2' => 'baz',
|
||||
]);
|
||||
$share->expects(self::once())
|
||||
->method('setProtocol')
|
||||
->with([
|
||||
'preservedValue1' => 'foobar',
|
||||
'preservedValue2' => 'baz',
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$this->federationFactory->expects(self::once())
|
||||
->method('getCloudFederationShare')
|
||||
->with(
|
||||
'remote1@nextcloud.remote',
|
||||
'cal1',
|
||||
'Calendar 1',
|
||||
'calendar',
|
||||
'host1@nextcloud.host',
|
||||
'Host 1',
|
||||
'host1@nextcloud.host',
|
||||
'Host 1',
|
||||
'token',
|
||||
'user',
|
||||
'calendar',
|
||||
)
|
||||
->willReturn($share);
|
||||
|
||||
$this->url->expects(self::once())
|
||||
->method('linkTo')
|
||||
->with('', 'remote.php')
|
||||
->willReturn('/remote.php');
|
||||
$this->url->expects(self::once())
|
||||
->method('getAbsoluteURL')
|
||||
->with('/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1')
|
||||
->willReturn('https://nextcloud.host/remote.php/dav/remote-calendars/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl/cal1_shared_by_host1');
|
||||
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getStatusCode')
|
||||
->willReturn(400);
|
||||
$this->federationManager->expects(self::once())
|
||||
->method('sendCloudShare')
|
||||
->with($share)
|
||||
->willReturn($response);
|
||||
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('deleteShare');
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('shareWithToken');
|
||||
|
||||
$this->federationSharingService->shareWith(
|
||||
$shareable,
|
||||
'principals/remote-users/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl',
|
||||
3, // Read-only
|
||||
);
|
||||
}
|
||||
|
||||
public static function provideInvalidRemoteUserPrincipalData(): array {
|
||||
return [
|
||||
['principals/users/foobar'],
|
||||
['remote-users/remote1'],
|
||||
['foobar/remote-users/remote1'],
|
||||
['principals/remote-groups/group1'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideInvalidRemoteUserPrincipalData')]
|
||||
public function testShareWithWithInvalidRemoteUserPrincipal(string $remoteUserPrincipal): void {
|
||||
$shareable = $this->createMock(Calendar::class);
|
||||
$shareable->method('getOwner')
|
||||
->willReturn('principals/users/host1');
|
||||
|
||||
$this->userManager->expects(self::never())
|
||||
->method('get');
|
||||
|
||||
$this->federationManager->expects(self::never())
|
||||
->method('sendCloudShare');
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('deleteShare');
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('shareWithToken');
|
||||
|
||||
$this->federationSharingService->shareWith(
|
||||
$shareable,
|
||||
$remoteUserPrincipal,
|
||||
3, // Read-only
|
||||
);
|
||||
}
|
||||
|
||||
public function testShareWithWithUnknownUser(): void {
|
||||
$shareable = $this->createMock(Calendar::class);
|
||||
$shareable->method('getOwner')
|
||||
->willReturn('principals/users/host1');
|
||||
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with('host1')
|
||||
->willReturn(null);
|
||||
|
||||
$this->federationManager->expects(self::never())
|
||||
->method('sendCloudShare');
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('deleteShare');
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('shareWithToken');
|
||||
|
||||
$this->federationSharingService->shareWith(
|
||||
$shareable,
|
||||
'principals/remote-users/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl',
|
||||
3, // Read-only
|
||||
);
|
||||
}
|
||||
|
||||
public function testShareWithWithInvalidShareable(): void {
|
||||
$shareable = $this->createMock(IShareable::class);
|
||||
$shareable->method('getOwner')
|
||||
->willReturn('principals/users/host1');
|
||||
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with('host1')
|
||||
->willReturn(null);
|
||||
|
||||
$this->federationManager->expects(self::never())
|
||||
->method('sendCloudShare');
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('deleteShare');
|
||||
$this->sharingMapper->expects(self::never())
|
||||
->method('shareWithToken');
|
||||
|
||||
$this->federationSharingService->shareWith(
|
||||
$shareable,
|
||||
'principals/remote-users/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl',
|
||||
3, // Read-only
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\Calendar;
|
||||
use OCA\DAV\CalDAV\Federation\RemoteUserCalendarHome;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\CalDAV\Backend\BackendInterface;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Test\TestCase;
|
||||
|
||||
class RemoteUserCalendarHomeTest extends TestCase {
|
||||
private RemoteUserCalendarHome $remoteUserCalendarHome;
|
||||
|
||||
private BackendInterface&MockObject $calDavBackend;
|
||||
private IL10N&MockObject $l10n;
|
||||
private IConfig&MockObject $config;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->calDavBackend = $this->createMock(BackendInterface::class);
|
||||
$this->l10n = $this->createMock(IL10N::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->remoteUserCalendarHome = new RemoteUserCalendarHome(
|
||||
$this->calDavBackend,
|
||||
[
|
||||
'uri' => 'principals/remote-users/abcdef123',
|
||||
],
|
||||
$this->l10n,
|
||||
$this->config,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetChild(): void {
|
||||
$calendar1 = [
|
||||
'id' => 10,
|
||||
'uri' => 'cal1',
|
||||
];
|
||||
$calendar2 = [
|
||||
'id' => 11,
|
||||
'uri' => 'cal2',
|
||||
];
|
||||
|
||||
$this->calDavBackend->expects(self::once())
|
||||
->method('getCalendarsForUser')
|
||||
->with('principals/remote-users/abcdef123')
|
||||
->willReturn([
|
||||
$calendar1,
|
||||
$calendar2,
|
||||
]);
|
||||
|
||||
$actual = $this->remoteUserCalendarHome->getChild('cal2');
|
||||
$this->assertInstanceOf(Calendar::class, $actual);
|
||||
$this->assertEquals(11, $actual->getResourceId());
|
||||
$this->assertEquals('cal2', $actual->getName());
|
||||
}
|
||||
|
||||
public function testGetChildNotFound(): void {
|
||||
$calendar1 = [
|
||||
'id' => 10,
|
||||
'uri' => 'cal1',
|
||||
];
|
||||
$calendar2 = [
|
||||
'id' => 11,
|
||||
'uri' => 'cal2',
|
||||
];
|
||||
|
||||
$this->calDavBackend->expects(self::once())
|
||||
->method('getCalendarsForUser')
|
||||
->with('principals/remote-users/abcdef123')
|
||||
->willReturn([
|
||||
$calendar1,
|
||||
$calendar2,
|
||||
]);
|
||||
|
||||
$this->expectException(NotFound::class);
|
||||
$this->remoteUserCalendarHome->getChild('cal3');
|
||||
}
|
||||
|
||||
public function testGetChildren(): void {
|
||||
$calendar1 = [
|
||||
'id' => 10,
|
||||
'uri' => 'cal1',
|
||||
];
|
||||
$calendar2 = [
|
||||
'id' => 11,
|
||||
'uri' => 'cal2',
|
||||
];
|
||||
|
||||
$this->calDavBackend->expects(self::once())
|
||||
->method('getCalendarsForUser')
|
||||
->with('principals/remote-users/abcdef123')
|
||||
->willReturn([
|
||||
$calendar1,
|
||||
$calendar2,
|
||||
]);
|
||||
|
||||
$actual = $this->remoteUserCalendarHome->getChildren();
|
||||
$this->assertInstanceOf(Calendar::class, $actual[0]);
|
||||
$this->assertEquals(10, $actual[0]->getResourceId());
|
||||
$this->assertEquals('cal1', $actual[0]->getName());
|
||||
$this->assertInstanceOf(Calendar::class, $actual[1]);
|
||||
$this->assertEquals(11, $actual[1]->getResourceId());
|
||||
$this->assertEquals('cal2', $actual[1]->getName());
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,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;
|
||||
|
|
@ -43,6 +44,8 @@ class PublicCalendarRootTest extends TestCase {
|
|||
private ISecureRandom $random;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
|
||||
protected FederatedCalendarMapper&MockObject $federatedCalendarMapper;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
|
|
@ -52,6 +55,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);
|
||||
|
|
@ -73,6 +77,7 @@ class PublicCalendarRootTest extends TestCase {
|
|||
$dispatcher,
|
||||
$config,
|
||||
$sharingBackend,
|
||||
$this->federatedCalendarMapper,
|
||||
false,
|
||||
);
|
||||
$this->l10n = $this->createMock(IL10N::class);
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@
|
|||
namespace OCA\DAV\Tests\unit\CardDAV;
|
||||
|
||||
use OC\KnownUser\KnownUserService;
|
||||
use OCA\DAV\CalDAV\Federation\FederationSharingService;
|
||||
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
|
||||
use OCA\DAV\CardDAV\AddressBook;
|
||||
use OCA\DAV\CardDAV\CardDavBackend;
|
||||
use OCA\DAV\CardDAV\Sharing\Backend;
|
||||
use OCA\DAV\CardDAV\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;
|
||||
|
|
@ -51,6 +53,8 @@ class CardDavBackendTest extends TestCase {
|
|||
private IGroupManager&MockObject $groupManager;
|
||||
private IEventDispatcher&MockObject $dispatcher;
|
||||
private IConfig&MockObject $config;
|
||||
private RemoteUserPrincipalBackend&MockObject $remoteUserPrincipalBackend;
|
||||
private FederationSharingService&MockObject $federationSharingService;
|
||||
private Backend $sharingBackend;
|
||||
private IDBConnection $db;
|
||||
private CardDavBackend $backend;
|
||||
|
|
@ -122,13 +126,17 @@ class CardDavBackendTest extends TestCase {
|
|||
->withAnyParameters()
|
||||
->willReturn([self::UNIT_TEST_GROUP]);
|
||||
$this->dispatcher = $this->createMock(IEventDispatcher::class);
|
||||
$this->remoteUserPrincipalBackend = $this->createMock(RemoteUserPrincipalBackend::class);
|
||||
$this->federationSharingService = $this->createMock(FederationSharingService::class);
|
||||
|
||||
$this->db = Server::get(IDBConnection::class);
|
||||
$this->sharingBackend = new Backend($this->userManager,
|
||||
$this->groupManager,
|
||||
$this->principal,
|
||||
$this->remoteUserPrincipalBackend,
|
||||
$this->createMock(ICacheFactory::class),
|
||||
new Service(new SharingMapper($this->db)),
|
||||
$this->federationSharingService,
|
||||
$this->createMock(LoggerInterface::class)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -66,13 +66,13 @@ class SyncServiceTest extends TestCase {
|
|||
->willReturn($this->client);
|
||||
|
||||
$this->service = new SyncService(
|
||||
$clientService,
|
||||
$this->config,
|
||||
$this->backend,
|
||||
$this->userManager,
|
||||
$this->dbConnection,
|
||||
$this->logger,
|
||||
$this->converter,
|
||||
$clientService,
|
||||
$this->config
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +314,7 @@ END:VCARD';
|
|||
$clientService = $this->createMock(IClientService::class);
|
||||
$config = $this->createMock(IConfig::class);
|
||||
|
||||
$ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config);
|
||||
$ss = new SyncService($clientService, $config, $backend, $userManager, $dbConnection, $logger, $converter);
|
||||
$ss->ensureSystemAddressBookExists('principals/users/adam', 'contacts', []);
|
||||
}
|
||||
|
||||
|
|
@ -359,7 +359,7 @@ END:VCARD';
|
|||
$clientService = $this->createMock(IClientService::class);
|
||||
$config = $this->createMock(IConfig::class);
|
||||
|
||||
$ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config);
|
||||
$ss = new SyncService($clientService, $config, $backend, $userManager, $dbConnection, $logger, $converter);
|
||||
$ss->updateUser($user);
|
||||
|
||||
$ss->updateUser($user);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\DAV\Tests\unit\DAV\Sharing;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederationSharingService;
|
||||
use OCA\DAV\CalDAV\Sharing\Backend as CalendarSharingBackend;
|
||||
use OCA\DAV\CalDAV\Sharing\Service;
|
||||
use OCA\DAV\CardDAV\Sharing\Backend as ContactsSharingBackend;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
|
||||
use OCA\DAV\DAV\Sharing\Backend;
|
||||
use OCA\DAV\DAV\Sharing\IShareable;
|
||||
use OCP\ICache;
|
||||
|
|
@ -32,6 +34,8 @@ class BackendTest extends TestCase {
|
|||
private LoggerInterface&MockObject $logger;
|
||||
private ICacheFactory&MockObject $cacheFactory;
|
||||
private Service&MockObject $calendarService;
|
||||
private RemoteUserPrincipalBackend&MockObject $remoteUserPrincipalBackend;
|
||||
private FederationSharingService&MockObject $federationSharingService;
|
||||
private CalendarSharingBackend $backend;
|
||||
|
||||
protected function setUp(): void {
|
||||
|
|
@ -47,13 +51,17 @@ class BackendTest extends TestCase {
|
|||
$this->cacheFactory->expects(self::any())
|
||||
->method('createInMemory')
|
||||
->willReturn($this->shareCache);
|
||||
$this->remoteUserPrincipalBackend = $this->createMock(RemoteUserPrincipalBackend::class);
|
||||
$this->federationSharingService = $this->createMock(FederationSharingService::class);
|
||||
|
||||
$this->backend = new CalendarSharingBackend(
|
||||
$this->userManager,
|
||||
$this->groupManager,
|
||||
$this->principalBackend,
|
||||
$this->remoteUserPrincipalBackend,
|
||||
$this->cacheFactory,
|
||||
$this->calendarService,
|
||||
$this->federationSharingService,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
|
@ -313,8 +321,10 @@ class BackendTest extends TestCase {
|
|||
$this->userManager,
|
||||
$this->groupManager,
|
||||
$this->principalBackend,
|
||||
$this->remoteUserPrincipalBackend,
|
||||
$this->cacheFactory,
|
||||
$service,
|
||||
$this->federationSharingService,
|
||||
$this->logger);
|
||||
$resourceId = 42;
|
||||
$principal = 'principals/groups/bob';
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
<commands>
|
||||
<command>OCA\Federation\Command\SyncFederationAddressBooks</command>
|
||||
<command>OCA\Federation\Command\SyncFederationCalendars</command>
|
||||
</commands>
|
||||
|
||||
<settings>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ return array(
|
|||
'OCA\\Federation\\BackgroundJob\\GetSharedSecret' => $baseDir . '/../lib/BackgroundJob/GetSharedSecret.php',
|
||||
'OCA\\Federation\\BackgroundJob\\RequestSharedSecret' => $baseDir . '/../lib/BackgroundJob/RequestSharedSecret.php',
|
||||
'OCA\\Federation\\Command\\SyncFederationAddressBooks' => $baseDir . '/../lib/Command/SyncFederationAddressBooks.php',
|
||||
'OCA\\Federation\\Command\\SyncFederationCalendars' => $baseDir . '/../lib/Command/SyncFederationCalendars.php',
|
||||
'OCA\\Federation\\Controller\\OCSAuthAPIController' => $baseDir . '/../lib/Controller/OCSAuthAPIController.php',
|
||||
'OCA\\Federation\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php',
|
||||
'OCA\\Federation\\DAV\\FedAuth' => $baseDir . '/../lib/DAV/FedAuth.php',
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class ComposerStaticInitFederation
|
|||
'OCA\\Federation\\BackgroundJob\\GetSharedSecret' => __DIR__ . '/..' . '/../lib/BackgroundJob/GetSharedSecret.php',
|
||||
'OCA\\Federation\\BackgroundJob\\RequestSharedSecret' => __DIR__ . '/..' . '/../lib/BackgroundJob/RequestSharedSecret.php',
|
||||
'OCA\\Federation\\Command\\SyncFederationAddressBooks' => __DIR__ . '/..' . '/../lib/Command/SyncFederationAddressBooks.php',
|
||||
'OCA\\Federation\\Command\\SyncFederationCalendars' => __DIR__ . '/..' . '/../lib/Command/SyncFederationCalendars.php',
|
||||
'OCA\\Federation\\Controller\\OCSAuthAPIController' => __DIR__ . '/..' . '/../lib/Controller/OCSAuthAPIController.php',
|
||||
'OCA\\Federation\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php',
|
||||
'OCA\\Federation\\DAV\\FedAuth' => __DIR__ . '/..' . '/../lib/DAV/FedAuth.php',
|
||||
|
|
|
|||
59
apps/federation/lib/Command/SyncFederationCalendars.php
Normal file
59
apps/federation/lib/Command/SyncFederationCalendars.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
namespace OCA\Federation\Command;
|
||||
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class SyncFederationCalendars extends Command {
|
||||
public function __construct(
|
||||
private readonly FederatedCalendarSyncService $syncService,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure() {
|
||||
$this
|
||||
->setName('federation:sync-calendars')
|
||||
->setDescription('Synchronize all incoming federated calendar shares');
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
$calendars = $this->federatedCalendarMapper->findAll();
|
||||
foreach ($calendars as $calendar) {
|
||||
try {
|
||||
$this->syncService->syncOne($calendar);
|
||||
} catch (\Exception $e) {
|
||||
$url = $calendar->getUri();
|
||||
$msg = $e->getMessage();
|
||||
$output->writeln("\n<error>Failed to sync calendar $url: $msg</error>");
|
||||
}
|
||||
|
||||
$progress->advance();
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
$output->writeln('');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -166,6 +166,10 @@ class ShareesAPIController extends OCSController {
|
|||
$shareTypes[] = IShare::TYPE_SCIENCEMESH;
|
||||
}
|
||||
|
||||
if ($itemType === 'calendar') {
|
||||
$shareTypes[] = IShare::TYPE_REMOTE;
|
||||
}
|
||||
|
||||
if ($shareType !== null && is_array($shareType)) {
|
||||
$shareTypes = array_intersect($shareTypes, $shareType);
|
||||
} elseif (is_numeric($shareType)) {
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ Feature: sharees
|
|||
| test (localhost) | 6 | test@localhost |
|
||||
Then "remotes" sharees returned is empty
|
||||
|
||||
Scenario: Remote sharee for calendars not allowed
|
||||
Scenario: Remote sharee for calendars
|
||||
Given As an "test"
|
||||
When getting sharees for
|
||||
| search | test@localhost |
|
||||
|
|
@ -240,7 +240,8 @@ Feature: sharees
|
|||
Then "users" sharees returned is empty
|
||||
Then "exact groups" sharees returned is empty
|
||||
Then "groups" sharees returned is empty
|
||||
Then "exact remotes" sharees returned is empty
|
||||
Then "exact remotes" sharees returned are
|
||||
| test (localhost) | 6 | test@localhost |
|
||||
Then "remotes" sharees returned is empty
|
||||
|
||||
Scenario: Group sharees not returned when group sharing is disabled
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ Feature: sharees_provisioningapiv2
|
|||
| test (localhost) | 6 | test@localhost |
|
||||
Then "remotes" sharees returned is empty
|
||||
|
||||
Scenario: Remote sharee for calendars not allowed
|
||||
Scenario: Remote sharee for calendars
|
||||
Given As an "test"
|
||||
When getting sharees for
|
||||
| search | test@localhost |
|
||||
|
|
@ -223,7 +223,8 @@ Feature: sharees_provisioningapiv2
|
|||
Then "users" sharees returned is empty
|
||||
Then "exact groups" sharees returned is empty
|
||||
Then "groups" sharees returned is empty
|
||||
Then "exact remotes" sharees returned is empty
|
||||
Then "exact remotes" sharees returned are
|
||||
| test (localhost) | 6 | test@localhost |
|
||||
Then "remotes" sharees returned is empty
|
||||
|
||||
Scenario: Group sharees not returned when group sharing is disabled
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@
|
|||
<DeprecatedMethod>
|
||||
<code><![CDATA[exec]]></code>
|
||||
<code><![CDATA[getAppValue]]></code>
|
||||
<code><![CDATA[getL10N]]></code>
|
||||
<code><![CDATA[getL10NFactory]]></code>
|
||||
</DeprecatedMethod>
|
||||
<UndefinedGlobalVariable>
|
||||
|
|
@ -824,6 +823,20 @@
|
|||
<code><![CDATA[null]]></code>
|
||||
</NullableReturnStatement>
|
||||
</file>
|
||||
<file src="apps/dav/lib/DAV/RemoteUserPrincipalBackend.php">
|
||||
<InvalidNullableReturnType>
|
||||
<code><![CDATA[getPrincipalByPath]]></code>
|
||||
</InvalidNullableReturnType>
|
||||
<NullableReturnStatement>
|
||||
<code><![CDATA[$principal]]></code>
|
||||
<code><![CDATA[null]]></code>
|
||||
</NullableReturnStatement>
|
||||
</file>
|
||||
<file src="apps/dav/lib/DAV/Sharing/Backend.php">
|
||||
<LessSpecificReturnType>
|
||||
<code><![CDATA[?array]]></code>
|
||||
</LessSpecificReturnType>
|
||||
</file>
|
||||
<file src="apps/dav/lib/DAV/Sharing/Plugin.php">
|
||||
<DeprecatedMethod>
|
||||
<code><![CDATA[getAppValue]]></code>
|
||||
|
|
|
|||
Loading…
Reference in a new issue