@ -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));
}
} 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,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");
}
$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.
*
@ -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;
}
}