From d6d8e9215c69a9e8d4bfa2035c657b0a037f3a2f Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Fri, 12 Mar 2021 11:20:04 +0100 Subject: [PATCH] Add a trashbin for calendars and calendar objects Signed-off-by: Christoph Wurst --- apps/dav/appinfo/info.xml | 4 +- apps/dav/appinfo/v1/caldav.php | 14 +- .../composer/composer/autoload_classmap.php | 14 + .../dav/composer/composer/autoload_static.php | 14 + apps/dav/lib/AppInfo/Application.php | 13 + .../BackgroundJob/CalendarRetentionJob.php | 48 ++ ...ateCalendarResourcesRoomsBackgroundJob.php | 5 +- apps/dav/lib/CalDAV/Activity/Backend.php | 22 +- .../lib/CalDAV/Activity/Provider/Calendar.php | 14 + .../lib/CalDAV/Activity/Provider/Event.php | 14 + apps/dav/lib/CalDAV/CalDavBackend.php | 430 +++++++++++++++--- apps/dav/lib/CalDAV/Calendar.php | 20 +- apps/dav/lib/CalDAV/CalendarHome.php | 20 +- apps/dav/lib/CalDAV/CalendarObject.php | 4 + apps/dav/lib/CalDAV/IRestorable.php | 41 ++ apps/dav/lib/CalDAV/RetentionService.php | 80 ++++ .../CalDAV/Trashbin/DeletedCalendarObject.php | 96 ++++ .../DeletedCalendarObjectsCollection.php | 131 ++++++ apps/dav/lib/CalDAV/Trashbin/Plugin.php | 96 ++++ .../dav/lib/CalDAV/Trashbin/RestoreTarget.php | 82 ++++ apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php | 129 ++++++ apps/dav/lib/Command/CreateCalendar.php | 14 +- .../lib/Command/RetentionCleanupCommand.php | 48 ++ .../lib/Events/CalendarMovedToTrashEvent.php | 82 ++++ .../CalendarObjectMovedToTrashEvent.php | 96 ++++ .../Events/CalendarObjectRestoredEvent.php | 96 ++++ apps/dav/lib/Events/CalendarRestoredEvent.php | 82 ++++ apps/dav/lib/HookManager.php | 5 +- .../lib/Listener/ActivityUpdaterListener.php | 99 +++- .../CalendarObjectReminderUpdaterListener.php | 78 +++- .../Version1018Date20210312100735.php | 45 ++ apps/dav/lib/RootCollection.php | 18 +- apps/dav/lib/Server.php | 3 +- .../SystemTagsObjectMappingCollection.php | 10 +- .../SystemTagsObjectTypeCollection.php | 9 +- apps/dav/tests/travis/caldav/install.sh | 6 +- .../unit/CalDAV/AbstractCalDavBackend.php | 15 +- .../tests/unit/CalDAV/CalDavBackendTest.php | 4 +- .../tests/unit/CalDAV/CalendarHomeTest.php | 12 +- .../unit/CalDAV/PublicCalendarRootTest.php | 6 +- .../features/bootstrap/CalDavContext.php | 3 + build/psalm-baseline.xml | 26 +- 42 files changed, 1938 insertions(+), 110 deletions(-) create mode 100644 apps/dav/lib/BackgroundJob/CalendarRetentionJob.php create mode 100644 apps/dav/lib/CalDAV/IRestorable.php create mode 100644 apps/dav/lib/CalDAV/RetentionService.php create mode 100644 apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php create mode 100644 apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php create mode 100644 apps/dav/lib/CalDAV/Trashbin/Plugin.php create mode 100644 apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php create mode 100644 apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php create mode 100644 apps/dav/lib/Command/RetentionCleanupCommand.php create mode 100644 apps/dav/lib/Events/CalendarMovedToTrashEvent.php create mode 100644 apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php create mode 100644 apps/dav/lib/Events/CalendarObjectRestoredEvent.php create mode 100644 apps/dav/lib/Events/CalendarRestoredEvent.php create mode 100644 apps/dav/lib/Migration/Version1018Date20210312100735.php diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index ff99623e38..c99d82a798 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ WebDAV WebDAV endpoint WebDAV endpoint - 1.17.2 + 1.18.0 agpl owncloud.org DAV @@ -24,6 +24,7 @@ OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob OCA\DAV\BackgroundJob\CleanupInvitationTokenJob OCA\DAV\BackgroundJob\EventReminderJob + OCA\DAV\BackgroundJob\CalendarRetentionJob @@ -48,6 +49,7 @@ OCA\DAV\Command\CreateCalendar OCA\DAV\Command\MoveCalendar OCA\DAV\Command\ListCalendars + OCA\DAV\Command\RetentionCleanupCommand OCA\DAV\Command\SendEventReminders OCA\DAV\Command\SyncBirthdayCalendar OCA\DAV\Command\SyncSystemAddressBook diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php index 236d81f66f..19842c91f0 100644 --- a/apps/dav/appinfo/v1/caldav.php +++ b/apps/dav/appinfo/v1/caldav.php @@ -61,8 +61,20 @@ $random = \OC::$server->getSecureRandom(); $logger = \OC::$server->getLogger(); $dispatcher = \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class); $legacyDispatcher = \OC::$server->getEventDispatcher(); +$config = \OC::$server->get(\OCP\IConfig::class); -$calDavBackend = new CalDavBackend($db, $principalBackend, $userManager, \OC::$server->getGroupManager(), $random, $logger, $dispatcher, $legacyDispatcher, true); +$calDavBackend = new CalDavBackend( + $db, + $principalBackend, + $userManager, + \OC::$server->getGroupManager(), + $random, + $logger, + $dispatcher, + $legacyDispatcher, + $config, + true +); $debugging = \OC::$server->getConfig()->getSystemValue('debug', false); $sendInvitations = \OC::$server->getConfig()->getAppValue('dav', 'sendInvitations', 'yes') === 'yes'; diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index bedb7b6ec7..19c0e2549f 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -13,6 +13,7 @@ return array( 'OCA\\DAV\\Avatars\\AvatarNode' => $baseDir . '/../lib/Avatars/AvatarNode.php', 'OCA\\DAV\\Avatars\\RootCollection' => $baseDir . '/../lib/Avatars/RootCollection.php', 'OCA\\DAV\\BackgroundJob\\BuildReminderIndexBackgroundJob' => $baseDir . '/../lib/BackgroundJob/BuildReminderIndexBackgroundJob.php', + 'OCA\\DAV\\BackgroundJob\\CalendarRetentionJob' => $baseDir . '/../lib/BackgroundJob/CalendarRetentionJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => $baseDir . '/../lib/BackgroundJob/CleanupDirectLinksJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', 'OCA\\DAV\\BackgroundJob\\EventReminderJob' => $baseDir . '/../lib/BackgroundJob/EventReminderJob.php', @@ -44,6 +45,7 @@ return array( 'OCA\\DAV\\CalDAV\\CalendarObject' => $baseDir . '/../lib/CalDAV/CalendarObject.php', 'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', + 'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php', 'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => $baseDir . '/../lib/CalDAV/Integration/ExternalCalendar.php', 'OCA\\DAV\\CalDAV\\Integration\\ICalendarProvider' => $baseDir . '/../lib/CalDAV/Integration/ICalendarProvider.php', 'OCA\\DAV\\CalDAV\\InvitationResponse\\InvitationResponseServer' => $baseDir . '/../lib/CalDAV/InvitationResponse/InvitationResponseServer.php', @@ -72,6 +74,7 @@ return array( 'OCA\\DAV\\CalDAV\\ResourceBooking\\AbstractPrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\ResourcePrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php', + 'OCA\\DAV\\CalDAV\\RetentionService' => $baseDir . '/../lib/CalDAV/RetentionService.php', 'OCA\\DAV\\CalDAV\\Schedule\\IMipPlugin' => $baseDir . '/../lib/CalDAV/Schedule/IMipPlugin.php', 'OCA\\DAV\\CalDAV\\Schedule\\Plugin' => $baseDir . '/../lib/CalDAV/Schedule/Plugin.php', 'OCA\\DAV\\CalDAV\\Search\\SearchPlugin' => $baseDir . '/../lib/CalDAV/Search/SearchPlugin.php', @@ -82,6 +85,11 @@ return array( 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => $baseDir . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => $baseDir . '/../lib/CalDAV/Trashbin/Plugin.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\RestoreTarget' => $baseDir . '/../lib/CalDAV/Trashbin/RestoreTarget.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\TrashbinHome' => $baseDir . '/../lib/CalDAV/Trashbin/TrashbinHome.php', 'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => $baseDir . '/../lib/CalDAV/WebcalCaching/Plugin.php', 'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => $baseDir . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php', 'OCA\\DAV\\Capabilities' => $baseDir . '/../lib/Capabilities.php', @@ -113,6 +121,7 @@ return array( 'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php', 'OCA\\DAV\\Command\\MoveCalendar' => $baseDir . '/../lib/Command/MoveCalendar.php', 'OCA\\DAV\\Command\\RemoveInvalidShares' => $baseDir . '/../lib/Command/RemoveInvalidShares.php', + 'OCA\\DAV\\Command\\RetentionCleanupCommand' => $baseDir . '/../lib/Command/RetentionCleanupCommand.php', 'OCA\\DAV\\Command\\SendEventReminders' => $baseDir . '/../lib/Command/SendEventReminders.php', 'OCA\\DAV\\Command\\SyncBirthdayCalendar' => $baseDir . '/../lib/Command/SyncBirthdayCalendar.php', 'OCA\\DAV\\Command\\SyncSystemAddressBook' => $baseDir . '/../lib/Command/SyncSystemAddressBook.php', @@ -188,10 +197,14 @@ return array( 'OCA\\DAV\\Events\\CachedCalendarObjectUpdatedEvent' => $baseDir . '/../lib/Events/CachedCalendarObjectUpdatedEvent.php', 'OCA\\DAV\\Events\\CalendarCreatedEvent' => $baseDir . '/../lib/Events/CalendarCreatedEvent.php', 'OCA\\DAV\\Events\\CalendarDeletedEvent' => $baseDir . '/../lib/Events/CalendarDeletedEvent.php', + 'OCA\\DAV\\Events\\CalendarMovedToTrashEvent' => $baseDir . '/../lib/Events/CalendarMovedToTrashEvent.php', 'OCA\\DAV\\Events\\CalendarObjectCreatedEvent' => $baseDir . '/../lib/Events/CalendarObjectCreatedEvent.php', 'OCA\\DAV\\Events\\CalendarObjectDeletedEvent' => $baseDir . '/../lib/Events/CalendarObjectDeletedEvent.php', + 'OCA\\DAV\\Events\\CalendarObjectMovedToTrashEvent' => $baseDir . '/../lib/Events/CalendarObjectMovedToTrashEvent.php', + 'OCA\\DAV\\Events\\CalendarObjectRestoredEvent' => $baseDir . '/../lib/Events/CalendarObjectRestoredEvent.php', 'OCA\\DAV\\Events\\CalendarObjectUpdatedEvent' => $baseDir . '/../lib/Events/CalendarObjectUpdatedEvent.php', 'OCA\\DAV\\Events\\CalendarPublishedEvent' => $baseDir . '/../lib/Events/CalendarPublishedEvent.php', + 'OCA\\DAV\\Events\\CalendarRestoredEvent' => $baseDir . '/../lib/Events/CalendarRestoredEvent.php', 'OCA\\DAV\\Events\\CalendarShareUpdatedEvent' => $baseDir . '/../lib/Events/CalendarShareUpdatedEvent.php', 'OCA\\DAV\\Events\\CalendarUnpublishedEvent' => $baseDir . '/../lib/Events/CalendarUnpublishedEvent.php', 'OCA\\DAV\\Events\\CalendarUpdatedEvent' => $baseDir . '/../lib/Events/CalendarUpdatedEvent.php', @@ -248,6 +261,7 @@ return array( 'OCA\\DAV\\Migration\\Version1012Date20190808122342' => $baseDir . '/../lib/Migration/Version1012Date20190808122342.php', 'OCA\\DAV\\Migration\\Version1016Date20201109085907' => $baseDir . '/../lib/Migration/Version1016Date20201109085907.php', 'OCA\\DAV\\Migration\\Version1017Date20210216083742' => $baseDir . '/../lib/Migration/Version1017Date20210216083742.php', + 'OCA\\DAV\\Migration\\Version1018Date20210312100735' => $baseDir . '/../lib/Migration/Version1018Date20210312100735.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', 'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 0a81ac8b4e..6895064e39 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -28,6 +28,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Avatars\\AvatarNode' => __DIR__ . '/..' . '/../lib/Avatars/AvatarNode.php', 'OCA\\DAV\\Avatars\\RootCollection' => __DIR__ . '/..' . '/../lib/Avatars/RootCollection.php', 'OCA\\DAV\\BackgroundJob\\BuildReminderIndexBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/BuildReminderIndexBackgroundJob.php', + 'OCA\\DAV\\BackgroundJob\\CalendarRetentionJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CalendarRetentionJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectLinksJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', 'OCA\\DAV\\BackgroundJob\\EventReminderJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/EventReminderJob.php', @@ -59,6 +60,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarObject.php', 'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', + 'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php', 'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ExternalCalendar.php', 'OCA\\DAV\\CalDAV\\Integration\\ICalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ICalendarProvider.php', 'OCA\\DAV\\CalDAV\\InvitationResponse\\InvitationResponseServer' => __DIR__ . '/..' . '/../lib/CalDAV/InvitationResponse/InvitationResponseServer.php', @@ -87,6 +89,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\ResourceBooking\\AbstractPrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\ResourcePrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php', + 'OCA\\DAV\\CalDAV\\RetentionService' => __DIR__ . '/..' . '/../lib/CalDAV/RetentionService.php', 'OCA\\DAV\\CalDAV\\Schedule\\IMipPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/IMipPlugin.php', 'OCA\\DAV\\CalDAV\\Schedule\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/Plugin.php', 'OCA\\DAV\\CalDAV\\Search\\SearchPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Search/SearchPlugin.php', @@ -97,6 +100,11 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/Plugin.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\RestoreTarget' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/RestoreTarget.php', + 'OCA\\DAV\\CalDAV\\Trashbin\\TrashbinHome' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/TrashbinHome.php', 'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/Plugin.php', 'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php', 'OCA\\DAV\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', @@ -128,6 +136,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php', 'OCA\\DAV\\Command\\MoveCalendar' => __DIR__ . '/..' . '/../lib/Command/MoveCalendar.php', 'OCA\\DAV\\Command\\RemoveInvalidShares' => __DIR__ . '/..' . '/../lib/Command/RemoveInvalidShares.php', + 'OCA\\DAV\\Command\\RetentionCleanupCommand' => __DIR__ . '/..' . '/../lib/Command/RetentionCleanupCommand.php', 'OCA\\DAV\\Command\\SendEventReminders' => __DIR__ . '/..' . '/../lib/Command/SendEventReminders.php', 'OCA\\DAV\\Command\\SyncBirthdayCalendar' => __DIR__ . '/..' . '/../lib/Command/SyncBirthdayCalendar.php', 'OCA\\DAV\\Command\\SyncSystemAddressBook' => __DIR__ . '/..' . '/../lib/Command/SyncSystemAddressBook.php', @@ -203,10 +212,14 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Events\\CachedCalendarObjectUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CachedCalendarObjectUpdatedEvent.php', 'OCA\\DAV\\Events\\CalendarCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarCreatedEvent.php', 'OCA\\DAV\\Events\\CalendarDeletedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarDeletedEvent.php', + 'OCA\\DAV\\Events\\CalendarMovedToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarMovedToTrashEvent.php', 'OCA\\DAV\\Events\\CalendarObjectCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectCreatedEvent.php', 'OCA\\DAV\\Events\\CalendarObjectDeletedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectDeletedEvent.php', + 'OCA\\DAV\\Events\\CalendarObjectMovedToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectMovedToTrashEvent.php', + 'OCA\\DAV\\Events\\CalendarObjectRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectRestoredEvent.php', 'OCA\\DAV\\Events\\CalendarObjectUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectUpdatedEvent.php', 'OCA\\DAV\\Events\\CalendarPublishedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarPublishedEvent.php', + 'OCA\\DAV\\Events\\CalendarRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarRestoredEvent.php', 'OCA\\DAV\\Events\\CalendarShareUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarShareUpdatedEvent.php', 'OCA\\DAV\\Events\\CalendarUnpublishedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarUnpublishedEvent.php', 'OCA\\DAV\\Events\\CalendarUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarUpdatedEvent.php', @@ -263,6 +276,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1012Date20190808122342' => __DIR__ . '/..' . '/../lib/Migration/Version1012Date20190808122342.php', 'OCA\\DAV\\Migration\\Version1016Date20201109085907' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date20201109085907.php', 'OCA\\DAV\\Migration\\Version1017Date20210216083742' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20210216083742.php', + 'OCA\\DAV\\Migration\\Version1018Date20210312100735' => __DIR__ . '/..' . '/../lib/Migration/Version1018Date20210312100735.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', 'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php', diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index d6c20f81d9..8dfbf62db2 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -57,9 +57,13 @@ use OCA\DAV\Events\AddressBookShareUpdatedEvent; use OCA\DAV\Events\AddressBookUpdatedEvent; use OCA\DAV\Events\CalendarCreatedEvent; use OCA\DAV\Events\CalendarDeletedEvent; +use OCA\DAV\Events\CalendarMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectCreatedEvent; use OCA\DAV\Events\CalendarObjectDeletedEvent; +use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; +use OCA\DAV\Events\CalendarObjectRestoredEvent; use OCA\DAV\Events\CalendarObjectUpdatedEvent; +use OCA\DAV\Events\CalendarRestoredEvent; use OCA\DAV\Events\CalendarShareUpdatedEvent; use OCA\DAV\Events\CalendarUpdatedEvent; use OCA\DAV\Events\CardCreatedEvent; @@ -129,7 +133,11 @@ class Application extends App implements IBootstrap { $context->registerEventListener(CalendarDeletedEvent::class, ActivityUpdaterListener::class); $context->registerEventListener(CalendarDeletedEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarDeletedEvent::class, CalendarDeletionDefaultUpdaterListener::class); + $context->registerEventListener(CalendarMovedToTrashEvent::class, ActivityUpdaterListener::class); + $context->registerEventListener(CalendarMovedToTrashEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarUpdatedEvent::class, ActivityUpdaterListener::class); + $context->registerEventListener(CalendarRestoredEvent::class, ActivityUpdaterListener::class); + $context->registerEventListener(CalendarRestoredEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarObjectCreatedEvent::class, ActivityUpdaterListener::class); $context->registerEventListener(CalendarObjectCreatedEvent::class, CalendarContactInteractionListener::class); $context->registerEventListener(CalendarObjectCreatedEvent::class, CalendarObjectReminderUpdaterListener::class); @@ -138,6 +146,10 @@ class Application extends App implements IBootstrap { $context->registerEventListener(CalendarObjectUpdatedEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarObjectDeletedEvent::class, ActivityUpdaterListener::class); $context->registerEventListener(CalendarObjectDeletedEvent::class, CalendarObjectReminderUpdaterListener::class); + $context->registerEventListener(CalendarObjectMovedToTrashEvent::class, ActivityUpdaterListener::class); + $context->registerEventListener(CalendarObjectMovedToTrashEvent::class, CalendarObjectReminderUpdaterListener::class); + $context->registerEventListener(CalendarObjectRestoredEvent::class, ActivityUpdaterListener::class); + $context->registerEventListener(CalendarObjectRestoredEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarShareUpdatedEvent::class, CalendarContactInteractionListener::class); @@ -220,6 +232,7 @@ class Application extends App implements IBootstrap { $syncService->updateUser($user); }); + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::updateShares', function (GenericEvent $event) use ($container) { /** @var Backend $backend */ $backend = $container->query(Backend::class); diff --git a/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php new file mode 100644 index 0000000000..5121505165 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php @@ -0,0 +1,48 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\BackgroundJob; + +use OCA\DAV\CalDAV\RetentionService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; + +class CalendarRetentionJob extends TimedJob { + /** @var RetentionService */ + private $service; + + public function __construct(ITimeFactory $time, + RetentionService $service) { + parent::__construct($time); + $this->service = $service; + + // Run four times a day + $this->setInterval(6 * 60 * 60); + } + + protected function run($argument): void { + $this->service->cleanUp(); + } +} diff --git a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php index 4ed4319d70..86d2d3ae35 100644 --- a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php @@ -415,7 +415,10 @@ class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob { CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI); if ($calendar !== null) { - $this->calDavBackend->deleteCalendar($calendar['id']); + $this->calDavBackend->deleteCalendar( + $calendar['id'], + true // Because this wasn't deleted by a user + ); } } diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php index 16f581db87..4b896de3b8 100644 --- a/apps/dav/lib/CalDAV/Activity/Backend.php +++ b/apps/dav/lib/CalDAV/Activity/Backend.php @@ -84,13 +84,33 @@ class Backend { $this->triggerCalendarActivity(Calendar::SUBJECT_UPDATE, $calendarData, $shares, $properties); } + /** + * Creates activities when a calendar was moved to trash + * + * @param array $calendarData + * @param array $shares + */ + public function onCalendarMovedToTrash(array $calendarData, array $shares): void { + $this->triggerCalendarActivity(Calendar::SUBJECT_MOVE_TO_TRASH, $calendarData, $shares); + } + + /** + * Creates activities when a calendar was restored + * + * @param array $calendarData + * @param array $shares + */ + public function onCalendarRestored(array $calendarData, array $shares): void { + $this->triggerCalendarActivity(Calendar::SUBJECT_RESTORE, $calendarData, $shares); + } + /** * Creates activities when a calendar was deleted * * @param array $calendarData * @param array $shares */ - public function onCalendarDelete(array $calendarData, array $shares) { + public function onCalendarDelete(array $calendarData, array $shares): void { $this->triggerCalendarActivity(Calendar::SUBJECT_DELETE, $calendarData, $shares); } diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php index 382825d895..a30c628a3a 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php @@ -39,6 +39,8 @@ use OCP\L10N\IFactory; class Calendar extends Base { public const SUBJECT_ADD = 'calendar_add'; public const SUBJECT_UPDATE = 'calendar_update'; + public const SUBJECT_MOVE_TO_TRASH = 'calendar_move_to_trash'; + public const SUBJECT_RESTORE = 'calendar_restore'; public const SUBJECT_DELETE = 'calendar_delete'; public const SUBJECT_PUBLISH = 'calendar_publish'; public const SUBJECT_UNPUBLISH = 'calendar_unpublish'; @@ -107,6 +109,14 @@ class Calendar extends Base { $subject = $this->l->t('{actor} updated calendar {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_UPDATE . '_self') { $subject = $this->l->t('You updated calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_MOVE_TO_TRASH) { + $subject = $this->l->t('{actor} deleted calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_MOVE_TO_TRASH . '_self') { + $subject = $this->l->t('You deleted calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_RESTORE) { + $subject = $this->l->t('{actor} restored calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_RESTORE . '_self') { + $subject = $this->l->t('You restored calendar {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_PUBLISH . '_self') { $subject = $this->l->t('You shared calendar {calendar} as public link'); } elseif ($event->getSubject() === self::SUBJECT_UNPUBLISH . '_self') { @@ -172,6 +182,10 @@ class Calendar extends Base { case self::SUBJECT_DELETE . '_self': case self::SUBJECT_UPDATE: case self::SUBJECT_UPDATE . '_self': + case self::SUBJECT_MOVE_TO_TRASH: + case self::SUBJECT_MOVE_TO_TRASH . '_self': + case self::SUBJECT_RESTORE: + case self::SUBJECT_RESTORE . '_self': case self::SUBJECT_PUBLISH . '_self': case self::SUBJECT_UNPUBLISH . '_self': case self::SUBJECT_SHARE_USER: diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Event.php b/apps/dav/lib/CalDAV/Activity/Provider/Event.php index 8850715a1c..03845cc4d8 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php @@ -40,6 +40,8 @@ use OCP\L10N\IFactory; class Event extends Base { public const SUBJECT_OBJECT_ADD = 'object_add'; public const SUBJECT_OBJECT_UPDATE = 'object_update'; + public const SUBJECT_OBJECT_MOVE_TO_TRASH = 'object_move_to_trash'; + public const SUBJECT_OBJECT_RESTORE = 'object_restore'; public const SUBJECT_OBJECT_DELETE = 'object_delete'; /** @var IFactory */ @@ -143,6 +145,14 @@ class Event extends Base { $subject = $this->l->t('{actor} updated event {event} in calendar {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_event_self') { $subject = $this->l->t('You updated event {event} in calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event') { + $subject = $this->l->t('{actor} deleted event {event} from calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self') { + $subject = $this->l->t('You deleted event {event} from calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event') { + $subject = $this->l->t('{actor} restored event {event} of calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event_self') { + $subject = $this->l->t('You restored event {event} of calendar {calendar}'); } else { throw new \InvalidArgumentException(); } @@ -169,6 +179,8 @@ class Event extends Base { case self::SUBJECT_OBJECT_ADD . '_event': case self::SUBJECT_OBJECT_DELETE . '_event': case self::SUBJECT_OBJECT_UPDATE . '_event': + case self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event': + case self::SUBJECT_OBJECT_RESTORE . '_event': return [ 'actor' => $this->generateUserParameter($parameters['actor']), 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), @@ -177,6 +189,8 @@ class Event extends Base { case self::SUBJECT_OBJECT_ADD . '_event_self': case self::SUBJECT_OBJECT_DELETE . '_event_self': case self::SUBJECT_OBJECT_UPDATE . '_event_self': + case self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self': + case self::SUBJECT_OBJECT_RESTORE . '_event_self': return [ 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), 'event' => $this->generateClassifiedObjectParameter($parameters['object']), diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 2daa03843d..53749855b8 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -39,6 +39,7 @@ namespace OCA\DAV\CalDAV; use DateTime; +use OCA\DAV\AppInfo\Application; use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\DAV\Sharing\Backend; use OCA\DAV\DAV\Sharing\IShareable; @@ -47,10 +48,14 @@ use OCA\DAV\Events\CachedCalendarObjectDeletedEvent; use OCA\DAV\Events\CachedCalendarObjectUpdatedEvent; use OCA\DAV\Events\CalendarCreatedEvent; use OCA\DAV\Events\CalendarDeletedEvent; +use OCA\DAV\Events\CalendarMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectCreatedEvent; use OCA\DAV\Events\CalendarObjectDeletedEvent; +use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; +use OCA\DAV\Events\CalendarObjectRestoredEvent; use OCA\DAV\Events\CalendarObjectUpdatedEvent; use OCA\DAV\Events\CalendarPublishedEvent; +use OCA\DAV\Events\CalendarRestoredEvent; use OCA\DAV\Events\CalendarShareUpdatedEvent; use OCA\DAV\Events\CalendarUnpublishedEvent; use OCA\DAV\Events\CalendarUpdatedEvent; @@ -59,12 +64,14 @@ use OCA\DAV\Events\SubscriptionDeletedEvent; use OCA\DAV\Events\SubscriptionUpdatedEvent; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\ILogger; use OCP\IUser; use OCP\IUserManager; use OCP\Security\ISecureRandom; +use RuntimeException; use Sabre\CalDAV\Backend\AbstractBackend; use Sabre\CalDAV\Backend\SchedulingSupport; use Sabre\CalDAV\Backend\SubscriptionSupport; @@ -72,6 +79,7 @@ use Sabre\CalDAV\Backend\SyncSupport; use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; use Sabre\DAV; +use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\PropPatch; @@ -87,6 +95,15 @@ use Sabre\VObject\Reader; use Sabre\VObject\Recur\EventIterator; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; +use function array_merge; +use function array_values; +use function explode; +use function is_array; +use function pathinfo; +use function sprintf; +use function str_replace; +use function strtolower; +use function time; /** * Class CalDavBackend @@ -134,6 +151,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => 'deleted_at', ]; /** @@ -191,6 +209,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** @var EventDispatcherInterface */ private $legacyDispatcher; + /** @var IConfig */ + private $config; + /** @var bool */ private $legacyEndpoint; @@ -218,6 +239,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ILogger $logger, IEventDispatcher $dispatcher, EventDispatcherInterface $legacyDispatcher, + IConfig $config, bool $legacyEndpoint = false) { $this->db = $db; $this->principalBackend = $principalBackend; @@ -227,6 +249,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->logger = $logger; $this->dispatcher = $dispatcher; $this->legacyDispatcher = $legacyDispatcher; + $this->config = $config; $this->legacyEndpoint = $legacyEndpoint; } @@ -261,6 +284,26 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $column; } + /** + * @return array{id: int, deleted_at: int}[] + */ + public function getDeletedCalendars(int $deletedBefore): array { + $qb = $this->db->getQueryBuilder(); + $qb->select(['id', 'deleted_at']) + ->from('calendars') + ->where($qb->expr()->isNotNull('deleted_at')) + ->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore))); + $result = $qb->executeQuery(); + $raw = $result->fetchAll(); + $result->closeCursor(); + return array_map(function ($row) { + return [ + 'id' => (int) $row['id'], + 'deleted_at' => (int) $row['deleted_at'], + ]; + }, $raw); + } + /** * Returns a list of calendars for a principal. * @@ -334,7 +377,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendar[$xmlName] = $row[$dbName]; } - $this->addOwnerPrincipal($calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); if (!isset($calendars[$calendar['id']])) { $calendars[$calendar['id']] = $calendar; @@ -410,7 +454,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendar[$xmlName] = $row[$dbName]; } - $this->addOwnerPrincipal($calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); $calendars[$calendar['id']] = $calendar; } @@ -458,7 +503,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendar[$xmlName] = $row[$dbName]; } - $this->addOwnerPrincipal($calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); if (!isset($calendars[$calendar['id']])) { $calendars[$calendar['id']] = $calendar; @@ -534,7 +580,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendar[$xmlName] = $row[$dbName]; } - $this->addOwnerPrincipal($calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); if (!isset($calendars[$calendar['id']])) { $calendars[$calendar['id']] = $calendar; @@ -601,7 +648,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendar[$xmlName] = $row[$dbName]; } - $this->addOwnerPrincipal($calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); return $calendar; } @@ -654,7 +702,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendar[$xmlName] = $row[$dbName]; } - $this->addOwnerPrincipal($calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); return $calendar; } @@ -705,7 +754,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendar[$xmlName] = $row[$dbName]; } - $this->addOwnerPrincipal($calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); return $calendar; } @@ -872,33 +922,84 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param mixed $calendarId * @return void */ - public function deleteCalendar($calendarId) { + public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) { + // The calendar is deleted right away if this is either enforced by the caller + // or the special contacts birthday calendar or when the preference of an empty + // retention (0 seconds) is set, which signals a disabled trashbin. $calendarData = $this->getCalendarById($calendarId); - $shares = $this->getShares($calendarId); + $isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI; + $trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0'; + if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) { + $calendarData = $this->getCalendarById($calendarId); + $shares = $this->getShares($calendarId); - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?'); - $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]); + $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder(); + $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable) + ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId))) + ->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))) + ->executeStatement(); - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?'); - $stmt->execute([$calendarId]); + $qbDeleteCalendarObjects = $this->db->getQueryBuilder(); + $qbDeleteCalendarObjects->delete('calendarobjects') + ->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId))) + ->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))) + ->executeStatement(); - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?'); - $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]); + $qbDeleteCalendarChanges = $this->db->getQueryBuilder(); + $qbDeleteCalendarObjects->delete('calendarchanges') + ->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId))) + ->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))) + ->executeStatement(); - $this->calendarSharingBackend->deleteAllShares($calendarId); + $this->calendarSharingBackend->deleteAllShares($calendarId); - $query = $this->db->getQueryBuilder(); - $query->delete($this->dbObjectPropertiesTable) - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))) - ->executeStatement(); + $qbDeleteCalendar = $this->db->getQueryBuilder(); + $qbDeleteCalendarObjects->delete('calendars') + ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId))) + ->executeStatement(); - // Only dispatch if we actually deleted anything - if ($calendarData) { - $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares)); + // Only dispatch if we actually deleted anything + if ($calendarData) { + $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares)); + } + } else { + $qbMarkCalendarDeleted = $this->db->getQueryBuilder(); + $qbMarkCalendarDeleted->update('calendars') + ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time())) + ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId))) + ->executeStatement(); + + $calendarData = $this->getCalendarById($calendarId); + $shares = $this->getShares($calendarId); + if ($calendarData) { + $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent( + (int)$calendarId, + $calendarData, + $shares + )); + } } } + public function restoreCalendar(int $id): void { + $qb = $this->db->getQueryBuilder(); + $update = $qb->update('calendars') + ->set('deleted_at', $qb->createNamedParameter(null)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); + $update->executeStatement(); + + $calendarData = $this->getCalendarById($id); + $shares = $this->getShares($id); + if ($calendarData === null) { + throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.'); + } + $this->dispatcher->dispatchTyped(new CalendarRestoredEvent( + $id, + $calendarData, + $shares + )); + } + /** * Delete all of an user's shares * @@ -946,7 +1047,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification']) ->from('calendarobjects') ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) + ->andWhere($query->expr()->isNull('deleted_at')); $stmt = $query->executeQuery(); $result = []; @@ -967,6 +1069,63 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $result; } + public function getDeletedCalendarObjects(int $deletedBefore): array { + $query = $this->db->getQueryBuilder(); + $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at']) + ->from('calendarobjects', 'co') + ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT)) + ->where($query->expr()->isNotNull('co.deleted_at')) + ->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore))); + $stmt = $query->executeQuery(); + + $result = []; + foreach ($stmt->fetchAll() as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => (int) $row['calendarid'], + 'calendartype' => (int) $row['calendartype'], + 'size' => (int) $row['size'], + 'component' => strtolower($row['componenttype']), + 'classification' => (int) $row['classification'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'], + ]; + } + $stmt->closeCursor(); + + return $result; + } + + public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array { + $query = $this->db->getQueryBuilder(); + $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at']) + ->from('calendarobjects', 'co') + ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->isNotNull('co.deleted_at')); + $stmt = $query->executeQuery(); + + $result = []; + while ($row = $stmt->fetch()) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'component' => strtolower($row['componenttype']), + 'classification' => (int)$row['classification'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'], + ]; + } + $stmt->closeCursor(); + + return $result; + } + /** * Returns information from a single calendar object, based on it's object * uri. @@ -984,7 +1143,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param int $calendarType * @return array|null */ - public function getCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) { + public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) { $query = $this->db->getQueryBuilder(); $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification']) ->from('calendarobjects') @@ -1038,7 +1197,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->from('calendarobjects') ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) + ->andWhere($query->expr()->isNull('deleted_at')); foreach ($chunks as $uris) { $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); @@ -1085,19 +1245,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { $extraData = $this->getDenormalizedData($calendarData); - $q = $this->db->getQueryBuilder(); - $q->select($q->func()->count('*')) + // Try to detect duplicates + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*')) ->from('calendarobjects') - ->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId))) - ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid']))) - ->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType))); - - $result = $q->executeQuery(); + ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId))) + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid']))) + ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) + ->andWhere($qb->expr()->isNull('deleted_at')); + $result = $qb->executeQuery(); $count = (int) $result->fetchOne(); $result->closeCursor(); if ($count !== 0) { - throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.'); + throw new BadRequest('Calendar object with uid already exists in this calendar collection.'); + } + // For a more specific error message we also try to explicitly look up the UID but as a deleted entry + $qbDel = $this->db->getQueryBuilder(); + $qbDel->select($qb->func()->count('*')) + ->from('calendarobjects') + ->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId))) + ->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid']))) + ->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType))) + ->andWhere($qbDel->expr()->isNotNull('deleted_at')); + $result = $qbDel->executeQuery(); + $count = (int) $result->fetchOne(); + $result->closeCursor(); + if ($count !== 0) { + throw new BadRequest('Deleted calendar object with uid already exists in this calendar collection.'); } $query = $this->db->getQueryBuilder(); @@ -1236,11 +1411,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param mixed $calendarId * @param string $objectUri * @param int $calendarType + * @param bool $forceDeletePermanently * @return void */ - public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) { + public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) { $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType); - if (is_array($data)) { + + if ($data === null) { + // Nothing to delete + return; + } + + if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') { + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?'); + $stmt->execute([$calendarId, $objectUri, $calendarType]); + + $this->purgeProperties($calendarId, $data['id']); + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { $calendarRow = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); @@ -1260,18 +1447,107 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ] )); } - } + } else { + $pathInfo = pathinfo($data['uri']); + if (!empty($pathInfo['extension'])) { + // Append a suffix to "free" the old URI for recreation + $newUri = sprintf( + "%s-deleted.%s", + $pathInfo['filename'], + $pathInfo['extension'] + ); + } else { + $newUri = sprintf( + "%s-deleted", + $pathInfo['filename'] + ); + } - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?'); - $stmt->execute([$calendarId, $objectUri, $calendarType]); + // Try to detect conflicts before the DB does + // As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again + $newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType); + if ($newObject !== null) { + throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin"); + } - if (is_array($data)) { - $this->purgeProperties($calendarId, $data['id'], $calendarType); + $qb = $this->db->getQueryBuilder(); + $markObjectDeletedQuery = $qb->update('calendarobjects') + ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT)) + ->set('uri', $qb->createNamedParameter($newUri)) + ->where( + $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), + $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), + $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri)) + ); + $markObjectDeletedQuery->executeStatement(); + + $calendarData = $this->getCalendarById($calendarId); + if ($calendarData !== null) { + $this->dispatcher->dispatchTyped( + new CalendarObjectMovedToTrashEvent( + (int)$calendarId, + $calendarData, + $this->getShares($calendarId), + $data + ) + ); + } } $this->addChange($calendarId, $objectUri, 3, $calendarType); } + /** + * @param mixed $objectData + * + * @throws Forbidden + */ + public function restoreCalendarObject(array $objectData): void { + $id = (int) $objectData['id']; + $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']); + $targetObject = $this->getCalendarObject( + $objectData['calendarid'], + $restoreUri + ); + if ($targetObject !== null) { + throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists"); + } + + $qb = $this->db->getQueryBuilder(); + $update = $qb->update('calendarobjects') + ->set('uri', $qb->createNamedParameter($restoreUri)) + ->set('deleted_at', $qb->createNamedParameter(null)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); + $update->executeStatement(); + + // Make sure this change is tracked in the changes table + $qb2 = $this->db->getQueryBuilder(); + $selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype') + ->from('calendarobjects') + ->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); + $result = $selectObject->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + if ($row === false) { + // Welp, this should possibly not have happened, but let's ignore + return; + } + $this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']); + + $calendarRow = $this->getCalendarById((int) $row['calendarid']); + if ($calendarRow === null) { + throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.'); + } + $this->dispatcher->dispatchTyped( + new CalendarObjectRestoredEvent( + (int) $objectData['calendarid'], + $calendarRow, + $this->getShares((int) $row['calendarid']), + $row + ) + ); + } + /** * Performs a calendar-query on the contents of this calendar. * @@ -1359,7 +1635,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $query->select($columns) ->from('calendarobjects') ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) + ->andWhere($query->expr()->isNull('deleted_at')); if ($componentType) { $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType))); @@ -1508,7 +1785,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->andWhere($compExpr) ->andWhere($propParamExpr) ->andWhere($query->expr()->iLike('i.value', - $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%'))); + $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%'))) + ->andWhere($query->expr()->isNull('deleted_at')); if ($offset) { $query->setFirstResult($offset); @@ -1574,7 +1852,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri') - ->from('calendarobjects', 'c'); + ->from('calendarobjects', 'c') + ->where($outerQuery->expr()->isNull('deleted_at')); if (isset($options['timerange'])) { if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) { @@ -1776,7 +2055,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid')) ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY))) ->andWhere($calendarOr) - ->andWhere($searchOr); + ->andWhere($searchOr) + ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at')); if ('' !== $pattern) { if (!$escapePattern) { @@ -1843,8 +2123,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->from('calendarobjects', 'co') ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id')) ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri))) - ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid))); - + ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid))) + ->andWhere($query->expr()->isNull('co.deleted_at')); $stmt = $query->executeQuery(); $row = $stmt->fetch(); $stmt->closeCursor(); @@ -1855,6 +2135,35 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return null; } + public function getCalendarObjectById(string $principalUri, int $id): ?array { + $query = $this->db->getQueryBuilder(); + $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at']) + ->from('calendarobjects', 'co') + ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); + $stmt = $query->executeQuery(); + $row = $stmt->fetch(); + $stmt->closeCursor(); + + if (!$row) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'calendardata' => $this->readBlob($row['calendardata']), + 'component' => strtolower($row['componenttype']), + 'classification' => (int)$row['classification'], + 'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null, + ]; + } + /** * The getChanges method returns all the changes that have happened, since * the specified syncToken in the specified calendar. @@ -2410,7 +2719,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } } if (!$componentType) { - throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); + throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); } if ($hasDTSTART) { @@ -2670,7 +2979,10 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $ids = $result->fetchAll(); foreach ($ids as $id) { - $this->deleteCalendar($id['id']); + $this->deleteCalendar( + $id['id'], + true // No data to keep in the trashbin, if the user re-enables then we regenerate + ); } } @@ -2802,9 +3114,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** * adds information about an owner to the calendar data * - * @param $calendarInfo */ - private function addOwnerPrincipal(&$calendarInfo) { + private function addOwnerPrincipalToCalendar(array $calendarInfo): array { $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'; $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'; if (isset($calendarInfo[$ownerPrincipalKey])) { @@ -2817,5 +3128,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription if (isset($principalInformation['{DAV:}displayname'])) { $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname']; } + return $calendarInfo; + } + + private function addResourceTypeToCalendar(array $row, array $calendar): array { + if (isset($row['deleted_at'])) { + // Columns is set and not null -> this is a deleted calendar + // we send a custom resourcetype to hide the deleted calendar + // from ordinary DAV clients, but the Calendar app will know + // how to handle this special resource. + $calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([ + '{DAV:}collection', + sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD), + ]); + } + return $calendar; } } diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php index abddf2b706..191ac384e7 100644 --- a/apps/dav/lib/CalDAV/Calendar.php +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -42,9 +42,9 @@ use Sabre\DAV\PropPatch; * Class Calendar * * @package OCA\DAV\CalDAV - * @property BackendInterface|CalDavBackend $caldavBackend + * @property CalDavBackend $caldavBackend */ -class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { +class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable { /** @var IConfig */ private $config; @@ -52,6 +52,9 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { /** @var IL10N */ protected $l10n; + /** @var bool */ + private $useTrashbin = true; + /** * Calendar constructor. * @@ -269,7 +272,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { $this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'no'); } - parent::delete(); + $this->caldavBackend->deleteCalendar( + $this->calendarInfo['id'], + !$this->useTrashbin + ); } public function propPatch(PropPatch $propPatch) { @@ -399,4 +405,12 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { return parent::getChanges($syncToken, $syncLevel, $limit); } + + public function restore(): void { + $this->caldavBackend->restoreCalendar((int) $this->calendarInfo['id']); + } + + public function disableTrashbin(): void { + $this->useTrashbin = false; + } } diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php index c746ab0411..c418ff049c 100644 --- a/apps/dav/lib/CalDAV/CalendarHome.php +++ b/apps/dav/lib/CalDAV/CalendarHome.php @@ -30,6 +30,7 @@ namespace OCA\DAV\CalDAV; use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Integration\ICalendarProvider; +use OCA\DAV\CalDAV\Trashbin\TrashbinHome; use Sabre\CalDAV\Backend\BackendInterface; use Sabre\CalDAV\Backend\NotificationSupport; use Sabre\CalDAV\Backend\SchedulingSupport; @@ -38,6 +39,7 @@ use Sabre\CalDAV\Schedule\Inbox; use Sabre\CalDAV\Subscriptions\Subscription; use Sabre\DAV\Exception\MethodNotAllowed; use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\INode; use Sabre\DAV\MkCol; class CalendarHome extends \Sabre\CalDAV\CalendarHome { @@ -74,8 +76,11 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { /** * @inheritdoc */ - public function createExtendedCollection($name, MkCol $mkCol) { - $reservedNames = [BirthdayService::BIRTHDAY_CALENDAR_URI]; + public function createExtendedCollection($name, MkCol $mkCol): void { + $reservedNames = [ + BirthdayService::BIRTHDAY_CALENDAR_URI, + TrashbinHome::NAME, + ]; if (\in_array($name, $reservedNames, true) || ExternalCalendar::doesViolateReservedName($name)) { throw new MethodNotAllowed('The resource you tried to create has a reserved name'); @@ -104,6 +109,10 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { $objects[] = new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); } + if ($this->caldavBackend instanceof CalDavBackend) { + $objects[] = new TrashbinHome($this->caldavBackend, $this->principalInfo); + } + // If the backend supports subscriptions, we'll add those as well, if ($this->caldavBackend instanceof SubscriptionSupport) { foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { @@ -127,7 +136,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { } /** - * @inheritdoc + * @param string $name + * + * @return INode */ public function getChild($name) { // Special nodes @@ -140,6 +151,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { if ($name === 'notifications' && $this->caldavBackend instanceof NotificationSupport) { return new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); } + if ($name === TrashbinHome::NAME && $this->caldavBackend instanceof CalDavBackend) { + return new TrashbinHome($this->caldavBackend, $this->principalInfo); + } // Calendars foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) { diff --git a/apps/dav/lib/CalDAV/CalendarObject.php b/apps/dav/lib/CalDAV/CalendarObject.php index 766062ffdf..0e42d9522a 100644 --- a/apps/dav/lib/CalDAV/CalendarObject.php +++ b/apps/dav/lib/CalDAV/CalendarObject.php @@ -82,6 +82,10 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject { return $vObject->serialize(); } + public function getId(): int { + return (int) $this->objectData['id']; + } + protected function isShared() { if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { return false; diff --git a/apps/dav/lib/CalDAV/IRestorable.php b/apps/dav/lib/CalDAV/IRestorable.php new file mode 100644 index 0000000000..438098b5a0 --- /dev/null +++ b/apps/dav/lib/CalDAV/IRestorable.php @@ -0,0 +1,41 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\CalDAV; + +use Sabre\DAV\Exception; + +/** + * Interface for nodes that can be restored from the trashbin + */ +interface IRestorable { + + /** + * Restore this node + * + * @throws Exception + */ + public function restore(): void; +} diff --git a/apps/dav/lib/CalDAV/RetentionService.php b/apps/dav/lib/CalDAV/RetentionService.php new file mode 100644 index 0000000000..934e6f7153 --- /dev/null +++ b/apps/dav/lib/CalDAV/RetentionService.php @@ -0,0 +1,80 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\CalDAV; + +use OCA\DAV\AppInfo\Application; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use function max; + +class RetentionService { + public const RETENTION_CONFIG_KEY = 'calendarRetentionObligation'; + private const DEFAULT_RETENTION_SECONDS = 30 * 24 * 60 * 60; + + /** @var IConfig */ + private $config; + + /** @var ITimeFactory */ + private $time; + + /** @var CalDavBackend */ + private $calDavBackend; + + public function __construct(IConfig $config, + ITimeFactory $time, + CalDavBackend $calDavBackend) { + $this->config = $config; + $this->time = $time; + $this->calDavBackend = $calDavBackend; + } + + public function cleanUp(): void { + $retentionTime = max( + (int) $this->config->getAppValue( + Application::APP_ID, + self::RETENTION_CONFIG_KEY, + (string) self::DEFAULT_RETENTION_SECONDS + ), + 0 // Just making sure we don't delete things in the future when a negative number is passed + ); + $now = $this->time->getTime(); + + $calendars = $this->calDavBackend->getDeletedCalendars($now - $retentionTime); + foreach ($calendars as $calendar) { + $this->calDavBackend->deleteCalendar($calendar['id'], true); + } + + $objects = $this->calDavBackend->getDeletedCalendarObjects($now - $retentionTime); + foreach ($objects as $object) { + $this->calDavBackend->deleteCalendarObject( + $object['calendarid'], + $object['uri'], + $object['calendartype'], + true + ); + } + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php new file mode 100644 index 0000000000..43d96b52ef --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php @@ -0,0 +1,96 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\CalDAV\Trashbin; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\IRestorable; +use Sabre\CalDAV\ICalendarObject; +use Sabre\DAV\Exception\Forbidden; + +class DeletedCalendarObject implements ICalendarObject, IRestorable { + + /** @var string */ + private $name; + + /** @var mixed[] */ + private $objectData; + + /** @var CalDavBackend */ + private $calDavBackend; + + public function __construct(string $name, + array $objectData, + CalDavBackend $calDavBackend) { + $this->name = $name; + $this->objectData = $objectData; + $this->calDavBackend = $calDavBackend; + } + + public function delete() { + throw new Forbidden(); + } + + public function getName() { + return $this->name; + } + + public function setName($name) { + throw new Forbidden(); + } + + public function getLastModified() { + return 0; + } + + public function put($data) { + throw new Forbidden(); + } + + public function get() { + return $this->objectData['calendardata']; + } + + public function getContentType() { + $mime = 'text/calendar; charset=utf-8'; + if (isset($this->objectData['component']) && $this->objectData['component']) { + $mime .= '; component='.$this->objectData['component']; + } + + return $mime; + } + + public function getETag() { + return $this->objectData['etag']; + } + + public function getSize() { + return (int) $this->objectData['size']; + } + + public function restore(): void { + $this->calDavBackend->restoreCalendarObject($this->objectData); + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php new file mode 100644 index 0000000000..2d79db03bc --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php @@ -0,0 +1,131 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\CalDAV\Trashbin; + +use OCA\DAV\CalDAV\CalDavBackend; +use Sabre\CalDAV\ICalendarObjectContainer; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\NotImplemented; +use function array_map; +use function implode; +use function preg_match; + +class DeletedCalendarObjectsCollection implements ICalendarObjectContainer { + public const NAME = 'objects'; + + /** @var CalDavBackend */ + protected $caldavBackend; + + /** @var mixed[] */ + private $principalInfo; + + public function __construct(CalDavBackend $caldavBackend, + array $principalInfo) { + $this->caldavBackend = $caldavBackend; + $this->principalInfo = $principalInfo; + } + + /** + * @see \OCA\DAV\CalDAV\Trashbin\DeletedCalendarObjectsCollection::calendarQuery + */ + public function getChildren() { + throw new NotImplemented(); + } + + public function getChild($name) { + if (!preg_match("/(\d+)\\.ics/", $name, $matches)) { + throw new NotFound(); + } + + $data = $this->caldavBackend->getCalendarObjectById( + $this->principalInfo['uri'], + (int) $matches[1], + ); + + // If the object hasn't been deleted yet then we don't want to find it here + if ($data === null) { + throw new NotFound(); + } + if (!isset($data['deleted_at'])) { + throw new BadRequest('The calendar object you\'re trying to restore is not marked as deleted'); + } + + return new DeletedCalendarObject( + $this->getRelativeObjectPath($data), + $data, + $this->caldavBackend + ); + } + + public function createFile($name, $data = null) { + throw new Forbidden(); + } + + public function createDirectory($name) { + throw new Forbidden(); + } + + public function childExists($name) { + try { + $this->getChild($name); + } catch (NotFound $e) { + return false; + } + + return true; + } + + public function delete() { + throw new Forbidden(); + } + + public function getName(): string { + return self::NAME; + } + + public function setName($name) { + throw new Forbidden(); + } + + public function getLastModified(): int { + return 0; + } + + public function calendarQuery(array $filters) { + return array_map(function (array $calendarInfo) { + return $this->getRelativeObjectPath($calendarInfo); + }, $this->caldavBackend->getDeletedCalendarObjectsByPrincipal($this->principalInfo['uri'])); + } + + private function getRelativeObjectPath(array $calendarInfo): string { + return implode( + '.', + [$calendarInfo['id'], 'ics'], + ); + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/Plugin.php b/apps/dav/lib/CalDAV/Trashbin/Plugin.php new file mode 100644 index 0000000000..d42a09f1c0 --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/Plugin.php @@ -0,0 +1,96 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\CalDAV\Trashbin; + +use OCA\DAV\CalDAV\Calendar; +use OCP\IRequest; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use function array_slice; +use function implode; + +/** + * Conditional logic to bypass the calendar trashbin + */ +class Plugin extends ServerPlugin { + + /** @var bool */ + private $disableTrashbin; + + /** @var Server */ + private $server; + + public function __construct(IRequest $request) { + $this->disableTrashbin = $request->getHeader('X-NC-CalDAV-No-Trashbin') === '1'; + } + + public function initialize(Server $server): void { + $this->server = $server; + $server->on('beforeMethod:*', [$this, 'beforeMethod']); + } + + public function beforeMethod(RequestInterface $request, ResponseInterface $response): void { + if (!$this->disableTrashbin) { + return; + } + + $path = $request->getPath(); + $pathParts = explode('/', ltrim($path, '/')); + if (\count($pathParts) < 3) { + // We are looking for a path like calendars/username/calendarname + return; + } + + // $calendarPath will look like calendars/username/calendarname + $calendarPath = implode( + '/', + array_slice($pathParts, 0, 3) + ); + try { + $calendar = $this->server->tree->getNodeForPath($calendarPath); + if (!($calendar instanceof Calendar)) { + // This is odd + return; + } + + /** @var Calendar $calendar */ + $calendar->disableTrashbin(); + } catch (NotFound $ex) { + return; + } + } + + public function getFeatures(): array { + return ['nc-calendar-trashbin']; + } + + public function getPluginName(): string { + return 'nc-calendar-trashbin'; + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php new file mode 100644 index 0000000000..0175168e8d --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php @@ -0,0 +1,82 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\CalDAV\Trashbin; + +use OCA\DAV\CalDAV\IRestorable; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; +use Sabre\DAV\IMoveTarget; +use Sabre\DAV\INode; + +class RestoreTarget implements ICollection, IMoveTarget { + public const NAME = 'restore'; + + public function createFile($name, $data = null) { + throw new Forbidden(); + } + + public function createDirectory($name) { + throw new Forbidden(); + } + + public function getChild($name) { + throw new NotFound(); + } + + public function getChildren(): array { + return []; + } + + public function childExists($name): bool { + return false; + } + + public function moveInto($targetName, $sourcePath, INode $sourceNode): bool { + if ($sourceNode instanceof IRestorable) { + $sourceNode->restore(); + return true; + } + + return false; + } + + public function delete() { + throw new Forbidden(); + } + + public function getName(): string { + return 'restore'; + } + + public function setName($name) { + throw new Forbidden(); + } + + public function getLastModified() { + return 0; + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php new file mode 100644 index 0000000000..87f1f24aaa --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php @@ -0,0 +1,129 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\CalDAV\Trashbin; + +use OCA\DAV\CalDAV\CalDavBackend; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; +use Sabre\DAV\INode; +use Sabre\DAV\IProperties; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Xml\Property\ResourceType; +use Sabre\DAVACL\ACLTrait; +use Sabre\DAVACL\IACL; +use function in_array; +use function sprintf; + +class TrashbinHome implements IACL, ICollection, IProperties { + use ACLTrait; + + public const NAME = 'trashbin'; + + /** @var CalDavBackend */ + private $caldavBackend; + + /** @var array */ + private $principalInfo; + + public function __construct(CalDavBackend $caldavBackend, + array $principalInfo) { + $this->caldavBackend = $caldavBackend; + $this->principalInfo = $principalInfo; + } + + public function getOwner(): string { + return $this->principalInfo['uri']; + } + + public function createFile($name, $data = null) { + throw new Forbidden('Permission denied to create files in the trashbin'); + } + + public function createDirectory($name) { + throw new Forbidden('Permission denied to create a directory in the trashbin'); + } + + public function getChild($name): INode { + switch ($name) { + case RestoreTarget::NAME: + return new RestoreTarget(); + case DeletedCalendarObjectsCollection::NAME: + return new DeletedCalendarObjectsCollection( + $this->caldavBackend, + $this->principalInfo + ); + } + + throw new NotFound(); + } + + public function getChildren(): array { + return [ + new RestoreTarget(), + new DeletedCalendarObjectsCollection( + $this->caldavBackend, + $this->principalInfo + ), + ]; + } + + public function childExists($name): bool { + return in_array($name, [ + RestoreTarget::NAME, + DeletedCalendarObjectsCollection::NAME, + ], true); + } + + public function delete() { + throw new Forbidden('Permission denied to delete the trashbin'); + } + + public function getName(): string { + return self::NAME; + } + + public function setName($name) { + throw new Forbidden('Permission denied to rename the trashbin'); + } + + public function getLastModified(): int { + return 0; + } + + public function propPatch(PropPatch $propPatch): void { + throw new Forbidden('not implemented'); + } + + public function getProperties($properties): array { + return [ + '{DAV:}resourcetype' => new ResourceType([ + '{DAV:}collection', + sprintf('{%s}trash-bin', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD), + ]), + ]; + } +} diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php index 1d543c71bc..5d0eede54b 100644 --- a/apps/dav/lib/Command/CreateCalendar.php +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -32,6 +32,7 @@ use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\Connector\Sabre\Principal; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUserManager; @@ -94,9 +95,20 @@ class CreateCalendar extends Command { $logger = \OC::$server->getLogger(); $dispatcher = \OC::$server->get(IEventDispatcher::class); $legacyDispatcher = \OC::$server->getEventDispatcher(); + $config = \OC::$server->get(IConfig::class); $name = $input->getArgument('name'); - $caldav = new CalDavBackend($this->dbConnection, $principalBackend, $this->userManager, $this->groupManager, $random, $logger, $dispatcher, $legacyDispatcher); + $caldav = new CalDavBackend( + $this->dbConnection, + $principalBackend, + $this->userManager, + $this->groupManager, + $random, + $logger, + $dispatcher, + $legacyDispatcher, + $config + ); $caldav->createCalendar("principals/users/$user", $name, []); return 0; } diff --git a/apps/dav/lib/Command/RetentionCleanupCommand.php b/apps/dav/lib/Command/RetentionCleanupCommand.php new file mode 100644 index 0000000000..a1f1d201d8 --- /dev/null +++ b/apps/dav/lib/Command/RetentionCleanupCommand.php @@ -0,0 +1,48 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\RetentionService; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class RetentionCleanupCommand extends Command { + /** @var RetentionService */ + private $service; + + public function __construct(RetentionService $service) { + parent::__construct('dav:retention:clean-up'); + + $this->service = $service; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->service->cleanUp(); + + return 0; + } +} diff --git a/apps/dav/lib/Events/CalendarMovedToTrashEvent.php b/apps/dav/lib/Events/CalendarMovedToTrashEvent.php new file mode 100644 index 0000000000..cc09ef0f07 --- /dev/null +++ b/apps/dav/lib/Events/CalendarMovedToTrashEvent.php @@ -0,0 +1,82 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Events; + +use OCP\EventDispatcher\Event; + +/** + * @since 22.0.0 + */ +class CalendarMovedToTrashEvent extends Event { + + /** @var int */ + private $calendarId; + + /** @var array */ + private $calendarData; + + /** @var array */ + private $shares; + + /** + * @param int $calendarId + * @param array $calendarData + * @param array $shares + * @since 22.0.0 + */ + public function __construct(int $calendarId, + array $calendarData, + array $shares) { + parent::__construct(); + $this->calendarId = $calendarId; + $this->calendarData = $calendarData; + $this->shares = $shares; + } + + /** + * @return int + * @since 22.0.0 + */ + public function getCalendarId(): int { + return $this->calendarId; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getCalendarData(): array { + return $this->calendarData; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getShares(): array { + return $this->shares; + } +} diff --git a/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php b/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php new file mode 100644 index 0000000000..91a1fed401 --- /dev/null +++ b/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php @@ -0,0 +1,96 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Events; + +use OCP\EventDispatcher\Event; + +/** + * @since 22.0.0 + */ +class CalendarObjectMovedToTrashEvent extends Event { + + /** @var int */ + private $calendarId; + + /** @var array */ + private $calendarData; + + /** @var array */ + private $shares; + + /** @var array */ + private $objectData; + + /** + * @param int $calendarId + * @param array $calendarData + * @param array $shares + * @param array $objectData + * @since 22.0.0 + */ + public function __construct(int $calendarId, + array $calendarData, + array $shares, + array $objectData) { + parent::__construct(); + $this->calendarId = $calendarId; + $this->calendarData = $calendarData; + $this->shares = $shares; + $this->objectData = $objectData; + } + + /** + * @return int + * @since 22.0.0 + */ + public function getCalendarId(): int { + return $this->calendarId; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getCalendarData(): array { + return $this->calendarData; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getShares(): array { + return $this->shares; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getObjectData(): array { + return $this->objectData; + } +} diff --git a/apps/dav/lib/Events/CalendarObjectRestoredEvent.php b/apps/dav/lib/Events/CalendarObjectRestoredEvent.php new file mode 100644 index 0000000000..d86c21e9ce --- /dev/null +++ b/apps/dav/lib/Events/CalendarObjectRestoredEvent.php @@ -0,0 +1,96 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Events; + +use OCP\EventDispatcher\Event; + +/** + * @since 22.0.0 + */ +class CalendarObjectRestoredEvent extends Event { + + /** @var int */ + private $calendarId; + + /** @var array */ + private $calendarData; + + /** @var array */ + private $shares; + + /** @var array */ + private $objectData; + + /** + * @param int $calendarId + * @param array $calendarData + * @param array $shares + * @param array $objectData + * @since 22.0.0 + */ + public function __construct(int $calendarId, + array $calendarData, + array $shares, + array $objectData) { + parent::__construct(); + $this->calendarId = $calendarId; + $this->calendarData = $calendarData; + $this->shares = $shares; + $this->objectData = $objectData; + } + + /** + * @return int + * @since 22.0.0 + */ + public function getCalendarId(): int { + return $this->calendarId; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getCalendarData(): array { + return $this->calendarData; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getShares(): array { + return $this->shares; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getObjectData(): array { + return $this->objectData; + } +} diff --git a/apps/dav/lib/Events/CalendarRestoredEvent.php b/apps/dav/lib/Events/CalendarRestoredEvent.php new file mode 100644 index 0000000000..3d4ac9cb86 --- /dev/null +++ b/apps/dav/lib/Events/CalendarRestoredEvent.php @@ -0,0 +1,82 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Events; + +use OCP\EventDispatcher\Event; + +/** + * @since 22.0.0 + */ +class CalendarRestoredEvent extends Event { + + /** @var int */ + private $calendarId; + + /** @var array */ + private $calendarData; + + /** @var array */ + private $shares; + + /** + * @param int $calendarId + * @param array $calendarData + * @param array $shares + * @since 22.0.0 + */ + public function __construct(int $calendarId, + array $calendarData, + array $shares) { + parent::__construct(); + $this->calendarId = $calendarId; + $this->calendarData = $calendarData; + $this->shares = $shares; + } + + /** + * @return int + * @since 22.0.0 + */ + public function getCalendarId(): int { + return $this->calendarId; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getCalendarData(): array { + return $this->calendarData; + } + + /** + * @return array + * @since 22.0.0 + */ + public function getShares(): array { + return $this->shares; + } +} diff --git a/apps/dav/lib/HookManager.php b/apps/dav/lib/HookManager.php index 558aad72c0..756d05bedb 100644 --- a/apps/dav/lib/HookManager.php +++ b/apps/dav/lib/HookManager.php @@ -128,7 +128,10 @@ class HookManager { } foreach ($this->calendarsToDelete as $calendar) { - $this->calDav->deleteCalendar($calendar['id']); + $this->calDav->deleteCalendar( + $calendar['id'], + true // Make sure the data doesn't go into the trashbin, a new user with the same UID would later see it otherwise + ); } $this->calDav->deleteAllSharesByUser('principals/users/' . $uid); diff --git a/apps/dav/lib/Listener/ActivityUpdaterListener.php b/apps/dav/lib/Listener/ActivityUpdaterListener.php index 30e0008b18..181a5717c9 100644 --- a/apps/dav/lib/Listener/ActivityUpdaterListener.php +++ b/apps/dav/lib/Listener/ActivityUpdaterListener.php @@ -26,11 +26,16 @@ declare(strict_types=1); namespace OCA\DAV\Listener; use OCA\DAV\CalDAV\Activity\Backend as ActivityBackend; +use OCA\DAV\DAV\Sharing\Plugin; use OCA\DAV\Events\CalendarCreatedEvent; use OCA\DAV\Events\CalendarDeletedEvent; +use OCA\DAV\Events\CalendarMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectCreatedEvent; use OCA\DAV\Events\CalendarObjectDeletedEvent; +use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; +use OCA\DAV\Events\CalendarObjectRestoredEvent; use OCA\DAV\Events\CalendarObjectUpdatedEvent; +use OCA\DAV\Events\CalendarRestoredEvent; use OCA\DAV\Events\CalendarUpdatedEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -85,16 +90,55 @@ class ActivityUpdaterListener implements IEventListener { 'exception' => $e, ]); } - } elseif ($event instanceof CalendarDeletedEvent) { + } elseif ($event instanceof CalendarMovedToTrashEvent) { try { - $this->activityBackend->onCalendarDelete( + $this->activityBackend->onCalendarMovedToTrash( $event->getCalendarData(), $event->getShares() ); $this->logger->debug( - sprintf('Activity generated for deleted calendar %d', $event->getCalendarId()) + sprintf('Activity generated for changed calendar %d', $event->getCalendarId()) ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the calendar update, so we just log it + $this->logger->error('Error generating activities for changed calendar: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } elseif ($event instanceof CalendarRestoredEvent) { + try { + $this->activityBackend->onCalendarRestored( + $event->getCalendarData(), + $event->getShares() + ); + + $this->logger->debug( + sprintf('Activity generated for changed calendar %d', $event->getCalendarId()) + ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the calendar update, so we just log it + $this->logger->error('Error generating activities for changed calendar: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } elseif ($event instanceof CalendarDeletedEvent) { + try { + $deletedProp = '{' . Plugin::NS_NEXTCLOUD . '}deleted-at'; + if (isset($event->getCalendarData()[$deletedProp])) { + $this->logger->debug( + sprintf('Calendar %d was already in trashbin, skipping deletion activity', $event->getCalendarId()) + ); + } else { + $this->activityBackend->onCalendarDelete( + $event->getCalendarData(), + $event->getShares() + ); + + $this->logger->debug( + sprintf('Activity generated for deleted calendar %d', $event->getCalendarId()) + ); + } } catch (Throwable $e) { // Any error with activities shouldn't abort the calendar deletion, so we just log it $this->logger->error('Error generating activities for a deleted calendar: ' . $e->getMessage(), [ @@ -137,18 +181,61 @@ class ActivityUpdaterListener implements IEventListener { 'exception' => $e, ]); } - } elseif ($event instanceof CalendarObjectDeletedEvent) { + } elseif ($event instanceof CalendarObjectMovedToTrashEvent) { try { $this->activityBackend->onTouchCalendarObject( - \OCA\DAV\CalDAV\Activity\Provider\Event::SUBJECT_OBJECT_DELETE, + \OCA\DAV\CalDAV\Activity\Provider\Event::SUBJECT_OBJECT_MOVE_TO_TRASH, $event->getCalendarData(), $event->getShares(), $event->getObjectData() ); $this->logger->debug( - sprintf('Activity generated for deleted calendar object %d', $event->getCalendarId()) + sprintf('Activity generated for a calendar object of calendar %d that is moved to trash', $event->getCalendarId()) ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the calendar object creation, so we just log it + $this->logger->error('Error generating activity for a new calendar object: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } elseif ($event instanceof CalendarObjectRestoredEvent) { + try { + $this->activityBackend->onTouchCalendarObject( + \OCA\DAV\CalDAV\Activity\Provider\Event::SUBJECT_OBJECT_RESTORE, + $event->getCalendarData(), + $event->getShares(), + $event->getObjectData() + ); + + $this->logger->debug( + sprintf('Activity generated for a restore calendar object of calendar %d', $event->getCalendarId()) + ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the calendar object restoration, so we just log it + $this->logger->error('Error generating activity for a restored calendar object: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } elseif ($event instanceof CalendarObjectDeletedEvent) { + try { + $deletedProp = '{' . Plugin::NS_NEXTCLOUD . '}deleted-at'; + if (isset($event->getObjectData()[$deletedProp])) { + $this->logger->debug( + sprintf('Calendar object in calendar %d was already in trashbin, skipping deletion activity', $event->getCalendarId()) + ); + } else { + $this->activityBackend->onTouchCalendarObject( + \OCA\DAV\CalDAV\Activity\Provider\Event::SUBJECT_OBJECT_DELETE, + $event->getCalendarData(), + $event->getShares(), + $event->getObjectData() + ); + + $this->logger->debug( + sprintf('Activity generated for deleted calendar object in calendar %d', $event->getCalendarId()) + ); + } } catch (Throwable $e) { // Any error with activities shouldn't abort the calendar deletion, so we just log it $this->logger->error('Error generating activity for a deleted calendar object: ' . $e->getMessage(), [ diff --git a/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php b/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php index b976ef3ad9..8ce6c02436 100644 --- a/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php +++ b/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php @@ -25,12 +25,17 @@ declare(strict_types=1); namespace OCA\DAV\Listener; +use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend; use OCA\DAV\CalDAV\Reminder\ReminderService; use OCA\DAV\Events\CalendarDeletedEvent; +use OCA\DAV\Events\CalendarMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectCreatedEvent; use OCA\DAV\Events\CalendarObjectDeletedEvent; +use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; +use OCA\DAV\Events\CalendarObjectRestoredEvent; use OCA\DAV\Events\CalendarObjectUpdatedEvent; +use OCA\DAV\Events\CalendarRestoredEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use Psr\Log\LoggerInterface; @@ -45,19 +50,39 @@ class CalendarObjectReminderUpdaterListener implements IEventListener { /** @var ReminderService */ private $reminderService; + /** @var CalDavBackend */ + private $calDavBackend; + /** @var LoggerInterface */ private $logger; public function __construct(ReminderBackend $reminderBackend, ReminderService $reminderService, + CalDavBackend $calDavBackend, LoggerInterface $logger) { $this->reminderBackend = $reminderBackend; $this->reminderService = $reminderService; + $this->calDavBackend = $calDavBackend; $this->logger = $logger; } public function handle(Event $event): void { - if ($event instanceof CalendarDeletedEvent) { + if ($event instanceof CalendarMovedToTrashEvent) { + try { + $this->reminderBackend->cleanRemindersForCalendar( + $event->getCalendarId() + ); + + $this->logger->debug( + sprintf('Reminders of calendar %d cleaned up after move into trashbin', $event->getCalendarId()) + ); + } catch (Throwable $e) { + // Any error with reminders shouldn't abort the calendar move, so we just log it + $this->logger->error('Error cleaning up reminders of a calendar moved into trashbin: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } elseif ($event instanceof CalendarDeletedEvent) { try { $this->reminderBackend->cleanRemindersForCalendar( $event->getCalendarId() @@ -72,6 +97,27 @@ class CalendarObjectReminderUpdaterListener implements IEventListener { 'exception' => $e, ]); } + } elseif ($event instanceof CalendarRestoredEvent) { + try { + $objects = $this->calDavBackend->getCalendarObjects($event->getCalendarId()); + $this->logger->debug(sprintf('Restoring calendar reminder objects for %d items', count($objects))); + foreach ($objects as $object) { + $fullObject = $this->calDavBackend->getCalendarObject( + $event->getCalendarId(), + $object['uri'] + ); + $this->reminderService->onCalendarObjectCreate($fullObject); + } + + $this->logger->debug( + sprintf('Reminders of calendar %d restored', $event->getCalendarId()) + ); + } catch (Throwable $e) { + // Any error with reminders shouldn't abort the calendar deletion, so we just log it + $this->logger->error('Error restoring reminders of a calendar: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } } elseif ($event instanceof CalendarObjectCreatedEvent) { try { $this->reminderService->onCalendarObjectCreate( @@ -102,6 +148,36 @@ class CalendarObjectReminderUpdaterListener implements IEventListener { 'exception' => $e, ]); } + } elseif ($event instanceof CalendarObjectMovedToTrashEvent) { + try { + $this->reminderService->onCalendarObjectDelete( + $event->getObjectData() + ); + + $this->logger->debug( + sprintf('Reminders of restored calendar object of calendar %d deleted', $event->getCalendarId()) + ); + } catch (Throwable $e) { + // Any error with reminders shouldn't abort the calendar object deletion, so we just log it + $this->logger->error('Error deleting reminders of a calendar object: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } elseif ($event instanceof CalendarObjectRestoredEvent) { + try { + $this->reminderService->onCalendarObjectCreate( + $event->getObjectData() + ); + + $this->logger->debug( + sprintf('Reminders of restored calendar object of calendar %d restored', $event->getCalendarId()) + ); + } catch (Throwable $e) { + // Any error with reminders shouldn't abort the calendar object deletion, so we just log it + $this->logger->error('Error restoring reminders of a calendar object: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } } elseif ($event instanceof CalendarObjectDeletedEvent) { try { $this->reminderService->onCalendarObjectDelete( diff --git a/apps/dav/lib/Migration/Version1018Date20210312100735.php b/apps/dav/lib/Migration/Version1018Date20210312100735.php new file mode 100644 index 0000000000..0bf160fb51 --- /dev/null +++ b/apps/dav/lib/Migration/Version1018Date20210312100735.php @@ -0,0 +1,45 @@ +getTable('calendars'); + $calendarsTable->addColumn('deleted_at', Types::INTEGER, [ + 'notnull' => false, + 'length' => 4, + 'unsigned' => true, + ]); + $calendarsTable->addIndex([ + 'principaluri', + 'deleted_at', + ], 'cals_princ_del_idx'); + + $calendarObjectsTable = $schema->getTable('calendarobjects'); + $calendarObjectsTable->addColumn('deleted_at', Types::INTEGER, [ + 'notnull' => false, + 'length' => 4, + 'unsigned' => true, + ]); + + return $schema; + } +} diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index 16a209a98f..dcc2256680 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -47,6 +47,7 @@ use OCA\DAV\Upload\CleanupService; use OCP\App\IAppManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use Sabre\DAV\SimpleCollection; class RootCollection extends SimpleCollection { @@ -62,6 +63,7 @@ class RootCollection extends SimpleCollection { $db = \OC::$server->getDatabaseConnection(); $dispatcher = \OC::$server->get(IEventDispatcher::class); $legacyDispatcher = \OC::$server->getEventDispatcher(); + $config = \OC::$server->get(IConfig::class); $proxyMapper = \OC::$server->query(ProxyMapper::class); $userPrincipalBackend = new Principal( @@ -95,15 +97,23 @@ class RootCollection extends SimpleCollection { $filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users'); $filesCollection->disableListing = $disableListing; - $caldavBackend = new CalDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $random, $logger, $dispatcher, $legacyDispatcher); + $caldavBackend = new CalDavBackend( + $db, + $userPrincipalBackend, + $userManager, + $groupManager, + $random, + $logger, + $dispatcher, + $legacyDispatcher, + $config + ); $userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users'); $userCalendarRoot->disableListing = $disableListing; - $resourceCalendarCaldavBackend = new CalDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $random, $logger, $dispatcher, $legacyDispatcher); $resourceCalendarRoot = new CalendarRoot($calendarResourcePrincipalBackend, $caldavBackend, 'principals/calendar-resources'); $resourceCalendarRoot->disableListing = $disableListing; - $roomCalendarCaldavBackend = new CalDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $random, $logger, $dispatcher, $legacyDispatcher); - $roomCalendarRoot = new CalendarRoot($calendarRoomPrincipalBackend, $roomCalendarCaldavBackend, 'principals/calendar-rooms'); + $roomCalendarRoot = new CalendarRoot($calendarRoomPrincipalBackend, $caldavBackend, 'principals/calendar-rooms'); $roomCalendarRoot->disableListing = $disableListing; $publicCalendarRoot = new PublicCalendarRoot($caldavBackend, $l10n, $config); diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index ba5bcf935e..0999da3dbe 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -165,7 +165,8 @@ class Server { $this->server->addPlugin(\OC::$server->query(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class)); } - $this->server->addPlugin(new CalDAV\WebcalCaching\Plugin($request)); + $this->server->addPlugin(new \OCA\DAV\CalDAV\Trashbin\Plugin($request)); + $this->server->addPlugin(new \OCA\DAV\CalDAV\WebcalCaching\Plugin($request)); $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin()); diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php index 0bd450467a..0216ddd1ae 100644 --- a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php @@ -115,20 +115,20 @@ class SystemTagsObjectMappingCollection implements ICollection { throw new Forbidden('Permission denied to create collections'); } - public function getChild($tagId) { + public function getChild($tagName) { try { - if ($this->tagMapper->haveTag([$this->objectId], $this->objectType, $tagId, true)) { - $tag = $this->tagManager->getTagsByIds([$tagId]); + if ($this->tagMapper->haveTag([$this->objectId], $this->objectType, $tagName, true)) { + $tag = $this->tagManager->getTagsByIds([$tagName]); $tag = current($tag); if ($this->tagManager->canUserSeeTag($tag, $this->user)) { return $this->makeNode($tag); } } - throw new NotFound('Tag with id ' . $tagId . ' not present for object ' . $this->objectId); + throw new NotFound('Tag with id ' . $tagName . ' not present for object ' . $this->objectId); } catch (\InvalidArgumentException $e) { throw new BadRequest('Invalid tag id', 0, $e); } catch (TagNotFoundException $e) { - throw new NotFound('Tag with id ' . $tagId . ' not found', 0, $e); + throw new NotFound('Tag with id ' . $tagName . ' not found', 0, $e); } } diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php index 683162cdc9..8f663cbe8f 100644 --- a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php @@ -115,17 +115,18 @@ class SystemTagsObjectTypeCollection implements ICollection { } /** - * @param string $objectId + * @param string $objectName + * * @return SystemTagsObjectMappingCollection * @throws NotFound */ - public function getChild($objectId) { + public function getChild($objectName) { // make sure the object exists and is reachable - if (!$this->childExists($objectId)) { + if (!$this->childExists($objectName)) { throw new NotFound('Entity does not exist or is not available'); } return new SystemTagsObjectMappingCollection( - $objectId, + $objectName, $this->objectType, $this->userSession->getUser(), $this->tagManager, diff --git a/apps/dav/tests/travis/caldav/install.sh b/apps/dav/tests/travis/caldav/install.sh index e0ac30c9e4..d6064fb7a3 100644 --- a/apps/dav/tests/travis/caldav/install.sh +++ b/apps/dav/tests/travis/caldav/install.sh @@ -11,8 +11,12 @@ if [ ! -f pycalendar/setup.py ]; then git clone https://github.com/apple/ccs-pycalendar.git pycalendar fi -# create test user cd "$SCRIPTPATH/../../../../../" + +# disable the trashbin, so recurrent deletion of the same object works +php occ config:app:set dav calendarRetentionObligation --value=0 + +# create test user OC_PASS=user01 php occ user:add --password-from-env user01 php occ dav:create-calendar user01 calendar php occ dav:create-calendar user01 shared diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php index 1264342e27..eb0530a8b3 100644 --- a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php +++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php @@ -113,7 +113,18 @@ abstract class AbstractCalDavBackend extends TestCase { $db = \OC::$server->getDatabaseConnection(); $this->random = \OC::$server->getSecureRandom(); $this->logger = $this->createMock(ILogger::class); - $this->backend = new CalDavBackend($db, $this->principal, $this->userManager, $this->groupManager, $this->random, $this->logger, $this->dispatcher, $this->legacyDispatcher); + $this->config = $this->createMock(IConfig::class); + $this->backend = new CalDavBackend( + $db, + $this->principal, + $this->userManager, + $this->groupManager, + $this->random, + $this->logger, + $this->dispatcher, + $this->legacyDispatcher, + $this->config + ); $this->cleanUpBackend(); } @@ -142,7 +153,7 @@ abstract class AbstractCalDavBackend extends TestCase { return $event instanceof CalendarDeletedEvent; })); foreach ($calendars as $calendar) { - $this->backend->deleteCalendar($calendar['id']); + $this->backend->deleteCalendar($calendar['id'], true); } $subscriptions = $this->backend->getSubscriptionsForUser($principal); foreach ($subscriptions as $subscription) { diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php index 2ac333b152..03abd554e2 100644 --- a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php +++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php @@ -83,7 +83,7 @@ class CalDavBackendTest extends AbstractCalDavBackend { ->with(self::callback(function ($event) { return $event instanceof CalendarDeletedEvent; })); - $this->backend->deleteCalendar($calendars[0]['id']); + $this->backend->deleteCalendar($calendars[0]['id'], true); $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); self::assertEmpty($calendars); } @@ -212,7 +212,7 @@ EOD; ->with(self::callback(function ($event) { return $event instanceof CalendarDeletedEvent; })); - $this->backend->deleteCalendar($calendars[0]['id']); + $this->backend->deleteCalendar($calendars[0]['id'], true); $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); self::assertEmpty($calendars); } diff --git a/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php b/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php index a1df2d6254..c9ef312df0 100644 --- a/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php +++ b/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php @@ -32,6 +32,7 @@ use OCA\DAV\CalDAV\CalendarHome; use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Integration\ICalendarProvider; use OCA\DAV\CalDAV\Outbox; +use OCA\DAV\CalDAV\Trashbin\TrashbinHome; use Sabre\CalDAV\Schedule\Inbox; use Sabre\DAV\MkCol; use Test\TestCase; @@ -141,13 +142,14 @@ class CalendarHomeTest extends TestCase { $actual = $this->calendarHome->getChildren(); - $this->assertCount(6, $actual); + $this->assertCount(7, $actual); $this->assertInstanceOf(Inbox::class, $actual[0]); $this->assertInstanceOf(Outbox::class, $actual[1]); - $this->assertEquals('plugin1calendar1', $actual[2]); - $this->assertEquals('plugin1calendar2', $actual[3]); - $this->assertEquals('plugin2calendar1', $actual[4]); - $this->assertEquals('plugin2calendar2', $actual[5]); + $this->assertInstanceOf(TrashbinHome::class, $actual[2]); + $this->assertEquals('plugin1calendar1', $actual[3]); + $this->assertEquals('plugin1calendar2', $actual[4]); + $this->assertEquals('plugin2calendar1', $actual[5]); + $this->assertEquals('plugin2calendar2', $actual[6]); } public function testGetChildNonAppGenerated():void { diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php index 5200d201f5..94e224e595 100644 --- a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php +++ b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php @@ -86,6 +86,7 @@ class PublicCalendarRootTest extends TestCase { $this->logger = $this->createMock(ILogger::class); $dispatcher = $this->createMock(IEventDispatcher::class); $legacyDispatcher = $this->createMock(EventDispatcherInterface::class); + $config = $this->createMock(IConfig::class); $this->principal->expects($this->any())->method('getGroupMembership') ->withAnyParameters() @@ -103,7 +104,8 @@ class PublicCalendarRootTest extends TestCase { $this->random, $this->logger, $dispatcher, - $legacyDispatcher + $legacyDispatcher, + $config ); $this->l10n = $this->getMockBuilder(IL10N::class) ->disableOriginalConstructor()->getMock(); @@ -129,7 +131,7 @@ class PublicCalendarRootTest extends TestCase { $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); foreach ($books as $book) { - $this->backend->deleteCalendar($book['id']); + $this->backend->deleteCalendar($book['id'], true); } } diff --git a/build/integration/features/bootstrap/CalDavContext.php b/build/integration/features/bootstrap/CalDavContext.php index b1981568e4..e01621359f 100644 --- a/build/integration/features/bootstrap/CalDavContext.php +++ b/build/integration/features/bootstrap/CalDavContext.php @@ -70,6 +70,9 @@ class CalDavContext implements \Behat\Behat\Context\Context { 'admin', 'admin', ], + 'headers' => [ + 'X-NC-CalDAV-No-Trashbin' => '1', + ] ] ); } catch (\GuzzleHttp\Exception\ClientException $e) { diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 225d191f64..5971cc18c3 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -163,11 +163,14 @@ '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject' + '\OCA\DAV\CalDAV\CalDavBackend::createSubscription' '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject' + '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription' '\OCA\DAV\CalDAV\CalDavBackend::publishCalendar' '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject' + '\OCA\DAV\CalDAV\CalDavBackend::updateShares' '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription' @@ -186,7 +189,7 @@ (int)$calendarId - + dispatch dispatch dispatch @@ -200,7 +203,7 @@ dispatch dispatch dispatch - purgeProperties + Uri\split($principalUri) @@ -210,16 +213,13 @@ - - $calendarPlugin->getCalendarInCalendarHome($this->principalInfo['uri'], $calendarUri) - new Inbox($this->caldavBackend, $this->principalInfo['uri']) - new Outbox($this->config, $this->principalInfo['uri']) - new Subscription($this->caldavBackend, $subscription) - new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']) - - - getChild - + + INode + + + INode + + $calendarPlugin->getCalendarInCalendarHome($this->principalInfo['uri'], $calendarUri) @@ -5095,4 +5095,4 @@ $e->getCode() - + \ No newline at end of file