Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2021-03-12 13:20:04 +0300
committerChristoph Wurst <christoph@winzerhof-wurst.at>2021-05-31 08:49:19 +0300
commitd6d8e9215c69a9e8d4bfa2035c657b0a037f3a2f (patch)
tree942254503672d6ab3e0b6c8fca0bd4053db352b9 /apps/dav/lib/CalDAV
parent9e596dd0cf455dc1639141c695f89c7d936f4c0c (diff)
Add a trashbin for calendars and calendar objects
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
Diffstat (limited to 'apps/dav/lib/CalDAV')
-rw-r--r--apps/dav/lib/CalDAV/Activity/Backend.php22
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Calendar.php14
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Event.php14
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php430
-rw-r--r--apps/dav/lib/CalDAV/Calendar.php20
-rw-r--r--apps/dav/lib/CalDAV/CalendarHome.php20
-rw-r--r--apps/dav/lib/CalDAV/CalendarObject.php4
-rw-r--r--apps/dav/lib/CalDAV/IRestorable.php41
-rw-r--r--apps/dav/lib/CalDAV/RetentionService.php80
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php96
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php131
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/Plugin.php96
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php82
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php129
14 files changed, 1120 insertions, 59 deletions
diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php
index 16f581db872..4b896de3b89 100644
--- a/apps/dav/lib/CalDAV/Activity/Backend.php
+++ b/apps/dav/lib/CalDAV/Activity/Backend.php
@@ -85,12 +85,32 @@ class Backend {
}
/**
+ * Creates activities when a calendar was moved to trash
+ *
+ * @param array $calendarData
+ * @param array $shares
+ */
+ public function onCalendarMovedToTrash(array $calendarData, array $shares): void {
+ $this->triggerCalendarActivity(Calendar::SUBJECT_MOVE_TO_TRASH, $calendarData, $shares);
+ }
+
+ /**
+ * Creates activities when a calendar was restored
+ *
+ * @param array $calendarData
+ * @param array $shares
+ */
+ public function onCalendarRestored(array $calendarData, array $shares): void {
+ $this->triggerCalendarActivity(Calendar::SUBJECT_RESTORE, $calendarData, $shares);
+ }
+
+ /**
* Creates activities when a calendar was deleted
*
* @param array $calendarData
* @param array $shares
*/
- public function onCalendarDelete(array $calendarData, array $shares) {
+ public function onCalendarDelete(array $calendarData, array $shares): void {
$this->triggerCalendarActivity(Calendar::SUBJECT_DELETE, $calendarData, $shares);
}
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
index 382825d8955..a30c628a3ad 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
@@ -39,6 +39,8 @@ use OCP\L10N\IFactory;
class Calendar extends Base {
public const SUBJECT_ADD = 'calendar_add';
public const SUBJECT_UPDATE = 'calendar_update';
+ public const SUBJECT_MOVE_TO_TRASH = 'calendar_move_to_trash';
+ public const SUBJECT_RESTORE = 'calendar_restore';
public const SUBJECT_DELETE = 'calendar_delete';
public const SUBJECT_PUBLISH = 'calendar_publish';
public const SUBJECT_UNPUBLISH = 'calendar_unpublish';
@@ -107,6 +109,14 @@ class Calendar extends Base {
$subject = $this->l->t('{actor} updated calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_UPDATE . '_self') {
$subject = $this->l->t('You updated calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_MOVE_TO_TRASH) {
+ $subject = $this->l->t('{actor} deleted calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_MOVE_TO_TRASH . '_self') {
+ $subject = $this->l->t('You deleted calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_RESTORE) {
+ $subject = $this->l->t('{actor} restored calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_RESTORE . '_self') {
+ $subject = $this->l->t('You restored calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_PUBLISH . '_self') {
$subject = $this->l->t('You shared calendar {calendar} as public link');
} elseif ($event->getSubject() === self::SUBJECT_UNPUBLISH . '_self') {
@@ -172,6 +182,10 @@ class Calendar extends Base {
case self::SUBJECT_DELETE . '_self':
case self::SUBJECT_UPDATE:
case self::SUBJECT_UPDATE . '_self':
+ case self::SUBJECT_MOVE_TO_TRASH:
+ case self::SUBJECT_MOVE_TO_TRASH . '_self':
+ case self::SUBJECT_RESTORE:
+ case self::SUBJECT_RESTORE . '_self':
case self::SUBJECT_PUBLISH . '_self':
case self::SUBJECT_UNPUBLISH . '_self':
case self::SUBJECT_SHARE_USER:
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Event.php b/apps/dav/lib/CalDAV/Activity/Provider/Event.php
index 8850715a1c5..03845cc4d87 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php
@@ -40,6 +40,8 @@ use OCP\L10N\IFactory;
class Event extends Base {
public const SUBJECT_OBJECT_ADD = 'object_add';
public const SUBJECT_OBJECT_UPDATE = 'object_update';
+ public const SUBJECT_OBJECT_MOVE_TO_TRASH = 'object_move_to_trash';
+ public const SUBJECT_OBJECT_RESTORE = 'object_restore';
public const SUBJECT_OBJECT_DELETE = 'object_delete';
/** @var IFactory */
@@ -143,6 +145,14 @@ class Event extends Base {
$subject = $this->l->t('{actor} updated event {event} in calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_event_self') {
$subject = $this->l->t('You updated event {event} in calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event') {
+ $subject = $this->l->t('{actor} deleted event {event} from calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self') {
+ $subject = $this->l->t('You deleted event {event} from calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event') {
+ $subject = $this->l->t('{actor} restored event {event} of calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event_self') {
+ $subject = $this->l->t('You restored event {event} of calendar {calendar}');
} else {
throw new \InvalidArgumentException();
}
@@ -169,6 +179,8 @@ class Event extends Base {
case self::SUBJECT_OBJECT_ADD . '_event':
case self::SUBJECT_OBJECT_DELETE . '_event':
case self::SUBJECT_OBJECT_UPDATE . '_event':
+ case self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event':
+ case self::SUBJECT_OBJECT_RESTORE . '_event':
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
@@ -177,6 +189,8 @@ class Event extends Base {
case self::SUBJECT_OBJECT_ADD . '_event_self':
case self::SUBJECT_OBJECT_DELETE . '_event_self':
case self::SUBJECT_OBJECT_UPDATE . '_event_self':
+ case self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self':
+ case self::SUBJECT_OBJECT_RESTORE . '_event_self':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'event' => $this->generateClassifiedObjectParameter($parameters['object']),
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php
index 2daa03843de..53749855b8c 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -39,6 +39,7 @@
namespace OCA\DAV\CalDAV;
use DateTime;
+use OCA\DAV\AppInfo\Application;
use OCA\DAV\Connector\Sabre\Principal;
use OCA\DAV\DAV\Sharing\Backend;
use OCA\DAV\DAV\Sharing\IShareable;
@@ -47,10 +48,14 @@ use OCA\DAV\Events\CachedCalendarObjectDeletedEvent;
use OCA\DAV\Events\CachedCalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarCreatedEvent;
use OCA\DAV\Events\CalendarDeletedEvent;
+use OCA\DAV\Events\CalendarMovedToTrashEvent;
use OCA\DAV\Events\CalendarObjectCreatedEvent;
use OCA\DAV\Events\CalendarObjectDeletedEvent;
+use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
+use OCA\DAV\Events\CalendarObjectRestoredEvent;
use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarPublishedEvent;
+use OCA\DAV\Events\CalendarRestoredEvent;
use OCA\DAV\Events\CalendarShareUpdatedEvent;
use OCA\DAV\Events\CalendarUnpublishedEvent;
use OCA\DAV\Events\CalendarUpdatedEvent;
@@ -59,12 +64,14 @@ use OCA\DAV\Events\SubscriptionDeletedEvent;
use OCA\DAV\Events\SubscriptionUpdatedEvent;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\ILogger;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
+use RuntimeException;
use Sabre\CalDAV\Backend\AbstractBackend;
use Sabre\CalDAV\Backend\SchedulingSupport;
use Sabre\CalDAV\Backend\SubscriptionSupport;
@@ -72,6 +79,7 @@ use Sabre\CalDAV\Backend\SyncSupport;
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
use Sabre\DAV;
+use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\PropPatch;
@@ -87,6 +95,15 @@ use Sabre\VObject\Reader;
use Sabre\VObject\Recur\EventIterator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
+use function array_merge;
+use function array_values;
+use function explode;
+use function is_array;
+use function pathinfo;
+use function sprintf;
+use function str_replace;
+use function strtolower;
+use function time;
/**
* Class CalDavBackend
@@ -134,6 +151,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
'{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
'{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => 'deleted_at',
];
/**
@@ -191,6 +209,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/** @var EventDispatcherInterface */
private $legacyDispatcher;
+ /** @var IConfig */
+ private $config;
+
/** @var bool */
private $legacyEndpoint;
@@ -218,6 +239,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
ILogger $logger,
IEventDispatcher $dispatcher,
EventDispatcherInterface $legacyDispatcher,
+ IConfig $config,
bool $legacyEndpoint = false) {
$this->db = $db;
$this->principalBackend = $principalBackend;
@@ -227,6 +249,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->legacyDispatcher = $legacyDispatcher;
+ $this->config = $config;
$this->legacyEndpoint = $legacyEndpoint;
}
@@ -262,6 +285,26 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * @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.
*
* Every project is an array with the following keys:
@@ -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));
+ }
+ } else {
+ $qbMarkCalendarDeleted = $this->db->getQueryBuilder();
+ $qbMarkCalendarDeleted->update('calendars')
+ ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
+ ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
+ ->executeStatement();
- // Only dispatch if we actually deleted anything
- if ($calendarData) {
- $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
+ $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,19 +1447,108 @@ 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");
+ }
+
+ $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();
- if (is_array($data)) {
- $this->purgeProperties($calendarId, $data['id'], $calendarType);
+ $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.
*
* The calendar-query is defined in RFC4791 : CalDAV. Using the
@@ -1359,7 +1635,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$query->select($columns)
->from('calendarobjects')
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
+ ->andWhere($query->expr()->isNull('deleted_at'));
if ($componentType) {
$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
@@ -1508,7 +1785,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->andWhere($compExpr)
->andWhere($propParamExpr)
->andWhere($query->expr()->iLike('i.value',
- $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
+ $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
+ ->andWhere($query->expr()->isNull('deleted_at'));
if ($offset) {
$query->setFirstResult($offset);
@@ -1574,7 +1852,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
- ->from('calendarobjects', 'c');
+ ->from('calendarobjects', 'c')
+ ->where($outerQuery->expr()->isNull('deleted_at'));
if (isset($options['timerange'])) {
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) {
@@ -1776,7 +2055,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
->andWhere($calendarOr)
- ->andWhere($searchOr);
+ ->andWhere($searchOr)
+ ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
if ('' !== $pattern) {
if (!$escapePattern) {
@@ -1843,8 +2123,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->from('calendarobjects', 'co')
->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
- ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
-
+ ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
+ ->andWhere($query->expr()->isNull('co.deleted_at'));
$stmt = $query->executeQuery();
$row = $stmt->fetch();
$stmt->closeCursor();
@@ -1855,6 +2135,35 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
+ public function getCalendarObjectById(string $principalUri, int $id): ?array {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
+ ->from('calendarobjects', 'co')
+ ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
+ ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
+ ->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ $stmt = $query->executeQuery();
+ $row = $stmt->fetch();
+ $stmt->closeCursor();
+
+ if (!$row) {
+ return null;
+ }
+
+ return [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"' . $row['etag'] . '"',
+ 'calendarid' => $row['calendarid'],
+ 'size' => (int)$row['size'],
+ 'calendardata' => $this->readBlob($row['calendardata']),
+ 'component' => strtolower($row['componenttype']),
+ 'classification' => (int)$row['classification'],
+ 'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
+ ];
+ }
+
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified calendar.
@@ -2410,7 +2719,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
if (!$componentType) {
- throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
+ throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
}
if ($hasDTSTART) {
@@ -2670,7 +2979,10 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$ids = $result->fetchAll();
foreach ($ids as $id) {
- $this->deleteCalendar($id['id']);
+ $this->deleteCalendar(
+ $id['id'],
+ true // No data to keep in the trashbin, if the user re-enables then we regenerate
+ );
}
}
@@ -2802,9 +3114,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* adds information about an owner to the calendar data
*
- * @param $calendarInfo
*/
- private function addOwnerPrincipal(&$calendarInfo) {
+ private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
if (isset($calendarInfo[$ownerPrincipalKey])) {
@@ -2817,5 +3128,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
if (isset($principalInformation['{DAV:}displayname'])) {
$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
}
+ return $calendarInfo;
+ }
+
+ private function addResourceTypeToCalendar(array $row, array $calendar): array {
+ if (isset($row['deleted_at'])) {
+ // Columns is set and not null -> this is a deleted calendar
+ // we send a custom resourcetype to hide the deleted calendar
+ // from ordinary DAV clients, but the Calendar app will know
+ // how to handle this special resource.
+ $calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
+ '{DAV:}collection',
+ sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
+ ]);
+ }
+ return $calendar;
}
}
diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php
index abddf2b706f..191ac384e72 100644
--- a/apps/dav/lib/CalDAV/Calendar.php
+++ b/apps/dav/lib/CalDAV/Calendar.php
@@ -42,9 +42,9 @@ use Sabre\DAV\PropPatch;
* Class Calendar
*
* @package OCA\DAV\CalDAV
- * @property BackendInterface|CalDavBackend $caldavBackend
+ * @property CalDavBackend $caldavBackend
*/
-class Calendar extends \Sabre\CalDAV\Calendar implements IShareable {
+class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable {
/** @var IConfig */
private $config;
@@ -52,6 +52,9 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable {
/** @var IL10N */
protected $l10n;
+ /** @var bool */
+ private $useTrashbin = true;
+
/**
* Calendar constructor.
*
@@ -269,7 +272,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable {
$this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'no');
}
- parent::delete();
+ $this->caldavBackend->deleteCalendar(
+ $this->calendarInfo['id'],
+ !$this->useTrashbin
+ );
}
public function propPatch(PropPatch $propPatch) {
@@ -399,4 +405,12 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable {
return parent::getChanges($syncToken, $syncLevel, $limit);
}
+
+ public function restore(): void {
+ $this->caldavBackend->restoreCalendar((int) $this->calendarInfo['id']);
+ }
+
+ public function disableTrashbin(): void {
+ $this->useTrashbin = false;
+ }
}
diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php
index c746ab04112..c418ff049c1 100644
--- a/apps/dav/lib/CalDAV/CalendarHome.php
+++ b/apps/dav/lib/CalDAV/CalendarHome.php
@@ -30,6 +30,7 @@ namespace OCA\DAV\CalDAV;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
+use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\CalDAV\Backend\NotificationSupport;
use Sabre\CalDAV\Backend\SchedulingSupport;
@@ -38,6 +39,7 @@ use Sabre\CalDAV\Schedule\Inbox;
use Sabre\CalDAV\Subscriptions\Subscription;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\INode;
use Sabre\DAV\MkCol;
class CalendarHome extends \Sabre\CalDAV\CalendarHome {
@@ -74,8 +76,11 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
/**
* @inheritdoc
*/
- public function createExtendedCollection($name, MkCol $mkCol) {
- $reservedNames = [BirthdayService::BIRTHDAY_CALENDAR_URI];
+ public function createExtendedCollection($name, MkCol $mkCol): void {
+ $reservedNames = [
+ BirthdayService::BIRTHDAY_CALENDAR_URI,
+ TrashbinHome::NAME,
+ ];
if (\in_array($name, $reservedNames, true) || ExternalCalendar::doesViolateReservedName($name)) {
throw new MethodNotAllowed('The resource you tried to create has a reserved name');
@@ -104,6 +109,10 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
$objects[] = new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
}
+ if ($this->caldavBackend instanceof CalDavBackend) {
+ $objects[] = new TrashbinHome($this->caldavBackend, $this->principalInfo);
+ }
+
// If the backend supports subscriptions, we'll add those as well,
if ($this->caldavBackend instanceof SubscriptionSupport) {
foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
@@ -127,7 +136,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
}
/**
- * @inheritdoc
+ * @param string $name
+ *
+ * @return INode
*/
public function getChild($name) {
// Special nodes
@@ -140,6 +151,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
if ($name === 'notifications' && $this->caldavBackend instanceof NotificationSupport) {
return new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
}
+ if ($name === TrashbinHome::NAME && $this->caldavBackend instanceof CalDavBackend) {
+ return new TrashbinHome($this->caldavBackend, $this->principalInfo);
+ }
// Calendars
foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) {
diff --git a/apps/dav/lib/CalDAV/CalendarObject.php b/apps/dav/lib/CalDAV/CalendarObject.php
index 766062ffdf1..0e42d9522ab 100644
--- a/apps/dav/lib/CalDAV/CalendarObject.php
+++ b/apps/dav/lib/CalDAV/CalendarObject.php
@@ -82,6 +82,10 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
return $vObject->serialize();
}
+ public function getId(): int {
+ return (int) $this->objectData['id'];
+ }
+
protected function isShared() {
if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return false;
diff --git a/apps/dav/lib/CalDAV/IRestorable.php b/apps/dav/lib/CalDAV/IRestorable.php
new file mode 100644
index 00000000000..438098b5a0e
--- /dev/null
+++ b/apps/dav/lib/CalDAV/IRestorable.php
@@ -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;
+}
diff --git a/apps/dav/lib/CalDAV/RetentionService.php b/apps/dav/lib/CalDAV/RetentionService.php
new file mode 100644
index 00000000000..934e6f71530
--- /dev/null
+++ b/apps/dav/lib/CalDAV/RetentionService.php
@@ -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
+ );
+ }
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
new file mode 100644
index 00000000000..43d96b52efd
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
@@ -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);
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
new file mode 100644
index 00000000000..2d79db03bce
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
@@ -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'],
+ );
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Trashbin/Plugin.php b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
new file mode 100644
index 00000000000..d42a09f1c0d
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
@@ -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';
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php
new file mode 100644
index 00000000000..0175168e8d2
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php
@@ -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;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
new file mode 100644
index 00000000000..87f1f24aaab
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
@@ -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),
+ ]),
+ ];
+ }
+}