Add a trashbin for calendars and calendar objects

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Christoph Wurst 2021-03-12 11:20:04 +01:00
parent 9e596dd0cf
commit d6d8e9215c
No known key found for this signature in database
GPG Key ID: CC42AC2A7F0E56D8
42 changed files with 1938 additions and 110 deletions

View File

@ -5,7 +5,7 @@
<name>WebDAV</name>
<summary>WebDAV endpoint</summary>
<description>WebDAV endpoint</description>
<version>1.17.2</version>
<version>1.18.0</version>
<licence>agpl</licence>
<author>owncloud.org</author>
<namespace>DAV</namespace>
@ -24,6 +24,7 @@
<job>OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob</job>
<job>OCA\DAV\BackgroundJob\CleanupInvitationTokenJob</job>
<job>OCA\DAV\BackgroundJob\EventReminderJob</job>
<job>OCA\DAV\BackgroundJob\CalendarRetentionJob</job>
</background-jobs>
<repair-steps>
@ -48,6 +49,7 @@
<command>OCA\DAV\Command\CreateCalendar</command>
<command>OCA\DAV\Command\MoveCalendar</command>
<command>OCA\DAV\Command\ListCalendars</command>
<command>OCA\DAV\Command\RetentionCleanupCommand</command>
<command>OCA\DAV\Command\SendEventReminders</command>
<command>OCA\DAV\Command\SyncBirthdayCalendar</command>
<command>OCA\DAV\Command\SyncSystemAddressBook</command>

View File

@ -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';

View File

@ -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',

View File

@ -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',

View File

@ -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);

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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
);
}
}

View File

@ -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);
}

View File

@ -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:

View File

@ -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']),

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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
);
}
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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'],
);
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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';
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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),
]),
];
}
}

View File

@ -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;
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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);

View File

@ -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(), [

View File

@ -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(

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace OCA\DAV\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version1018Date20210312100735 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$calendarsTable = $schema->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;
}
}

View File

@ -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);

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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

View File

@ -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) {

View File

@ -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);
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -163,11 +163,14 @@
<file src="apps/dav/lib/CalDAV/CalDavBackend.php">
<InvalidArgument occurrences="8">
<code>'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject'</code>
<code>'\OCA\DAV\CalDAV\CalDavBackend::createSubscription'</code>
<code>'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject'</code>
<code>'\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription'</code>
<code>'\OCA\DAV\CalDAV\CalDavBackend::publishCalendar'</code>
<code>'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject'</code>
<code>'\OCA\DAV\CalDAV\CalDavBackend::updateShares'</code>
<code>'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription'</code>
</InvalidArgument>
@ -186,7 +189,7 @@
<RedundantCast occurrences="1">
<code>(int)$calendarId</code>
</RedundantCast>
<TooManyArguments occurrences="9">
<TooManyArguments occurrences="13">
<code>dispatch</code>
<code>dispatch</code>
<code>dispatch</code>
@ -200,7 +203,7 @@
<code>dispatch</code>
<code>dispatch</code>
<code>dispatch</code>
<code>purgeProperties</code>
</TooManyArguments>
<UndefinedFunction occurrences="4">
<code>Uri\split($principalUri)</code>
@ -210,16 +213,13 @@
</UndefinedFunction>
</file>
<file src="apps/dav/lib/CalDAV/CalendarHome.php">
<InvalidReturnStatement occurrences="5">
<code>$calendarPlugin-&gt;getCalendarInCalendarHome($this-&gt;principalInfo['uri'], $calendarUri)</code>
<code>new Inbox($this-&gt;caldavBackend, $this-&gt;principalInfo['uri'])</code>
<code>new Outbox($this-&gt;config, $this-&gt;principalInfo['uri'])</code>
<code>new Subscription($this-&gt;caldavBackend, $subscription)</code>
<code>new \Sabre\CalDAV\Notifications\Collection($this-&gt;caldavBackend, $this-&gt;principalInfo['uri'])</code>
</InvalidReturnStatement>
<InvalidReturnType occurrences="1">
<code>getChild</code>
</InvalidReturnType>
<InvalidNullableReturnType occurrences="1">
<code>INode</code>
</InvalidNullableReturnType>
<LessSpecificImplementedReturnType occurrences="1">
<code>INode</code>
</LessSpecificImplementedReturnType>
<NullableReturnStatement occurrences="1">
<code>$calendarPlugin-&gt;getCalendarInCalendarHome($this-&gt;principalInfo['uri'], $calendarUri)</code>
</NullableReturnStatement>
@ -5095,4 +5095,4 @@
<code>$e-&gt;getCode()</code>
</InvalidScalarArgument>
</file>
</files>
</files>