diff options
Diffstat (limited to 'apps/dav/lib/CalDAV/CalDavBackend.php')
-rw-r--r-- | apps/dav/lib/CalDAV/CalDavBackend.php | 726 |
1 files changed, 479 insertions, 247 deletions
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index cc6c4344c3c..d5b0d875ede 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -9,6 +10,7 @@ namespace OCA\DAV\CalDAV; use DateTime; use DateTimeImmutable; use DateTimeInterface; +use Generator; use OCA\DAV\AppInfo\Application; use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\Connector\Sabre\Principal; @@ -19,12 +21,6 @@ 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\CalendarObjectMovedEvent; -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; @@ -34,6 +30,13 @@ use OCA\DAV\Events\SubscriptionCreatedEvent; use OCA\DAV\Events\SubscriptionDeletedEvent; use OCA\DAV\Events\SubscriptionUpdatedEvent; use OCP\AppFramework\Db\TTransactional; +use OCP\Calendar\CalendarExportOptions; +use OCP\Calendar\Events\CalendarObjectCreatedEvent; +use OCP\Calendar\Events\CalendarObjectDeletedEvent; +use OCP\Calendar\Events\CalendarObjectMovedEvent; +use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent; +use OCP\Calendar\Events\CalendarObjectRestoredEvent; +use OCP\Calendar\Events\CalendarObjectUpdatedEvent; use OCP\Calendar\Exceptions\CalendarException; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -65,6 +68,8 @@ use Sabre\VObject\ParseException; use Sabre\VObject\Property; use Sabre\VObject\Reader; use Sabre\VObject\Recur\EventIterator; +use Sabre\VObject\Recur\MaxInstancesExceededException; +use Sabre\VObject\Recur\NoInstancesException; use function array_column; use function array_map; use function array_merge; @@ -86,6 +91,19 @@ use function time; * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php * * @package OCA\DAV\CalDAV + * + * @psalm-type CalendarInfo = array{ + * id: int, + * uri: string, + * principaluri: string, + * '{http://calendarserver.org/ns/}getctag': string, + * '{http://sabredav.org/ns}sync-token': int, + * '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet, + * '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': \Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp, + * '{DAV:}displayname': string, + * '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string, + * '{http://nextcloud.com/ns}owner-displayname': string, + * } */ class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport { use TTransactional; @@ -176,7 +194,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ protected array $userDisplayNames; + private string $dbObjectsTable = 'calendarobjects'; private string $dbObjectPropertiesTable = 'calendarobjects_props'; + private string $dbObjectInvitationsTable = 'calendar_invitations'; private array $cachedObjects = []; public function __construct( @@ -193,15 +213,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * Return the number of calendars for a principal + * Return the number of calendars owned by the given principal. * - * By default this excludes the automatically generated birthday calendar + * Calendars shared with the given principal are not counted! * - * @param $principalUri - * @param bool $excludeBirthday - * @return int + * By default, this excludes the automatically generated birthday calendar. */ - public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) { + public function getCalendarsForUserCount(string $principalUri, bool $excludeBirthday = true): int { $principalUri = $this->convertPrincipal($principalUri, true); $query = $this->db->getQueryBuilder(); $query->select($query->func()->count('*')) @@ -257,8 +275,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendars = []; while (($row = $result->fetch()) !== false) { $calendars[] = [ - 'id' => (int) $row['id'], - 'deleted_at' => (int) $row['deleted_at'], + 'id' => (int)$row['id'], + 'deleted_at' => (int)$row['deleted_at'], ]; } $result->closeCursor(); @@ -318,7 +336,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendars = []; while ($row = $result->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $components = []; if ($row['components']) { $components = explode(',', $row['components']); @@ -328,8 +346,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint), @@ -352,7 +370,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $fields = array_column($this->propertyMap, 0); $fields = array_map(function (string $field) { - return 'a.'.$field; + return 'a.' . $field; }, $fields); $fields[] = 'a.id'; $fields[] = 'a.uri'; @@ -381,19 +399,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; while ($row = $results->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; if ($row['principaluri'] === $principalUri) { continue; } - $readOnly = (int) $row['access'] === Backend::ACCESS_READ; + $readOnly = (int)$row['access'] === Backend::ACCESS_READ; if (isset($calendars[$row['id']])) { if ($readOnly) { // New share can not have more permissions than the old one. continue; } - if (isset($calendars[$row['id']][$readOnlyPropertyName]) && - $calendars[$row['id']][$readOnlyPropertyName] === 0) { + if (isset($calendars[$row['id']][$readOnlyPropertyName]) + && $calendars[$row['id']][$readOnlyPropertyName] === 0) { // Old share is already read-write, no more permissions can be gained continue; } @@ -410,8 +428,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $uri, 'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'), '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), @@ -451,7 +469,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $stmt = $query->executeQuery(); $calendars = []; while ($row = $stmt->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $components = []; if ($row['components']) { $components = explode(',', $row['components']); @@ -460,8 +478,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), ]; @@ -501,7 +519,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->executeQuery(); while ($row = $result->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; [, $name] = Uri\split($row['principaluri']); $row['displayname'] = $row['displayname'] . "($name)"; $components = []; @@ -512,8 +530,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['publicuri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint), @@ -566,7 +584,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription throw new NotFound('Node with name \'' . $uri . '\' could not be found'); } - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; [, $name] = Uri\split($row['principaluri']); $row['displayname'] = $row['displayname'] . ' ' . "($name)"; $components = []; @@ -577,8 +595,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['publicuri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), @@ -621,7 +639,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return null; } - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $components = []; if ($row['components']) { $components = explode(',', $row['components']); @@ -631,8 +649,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), ]; @@ -645,7 +663,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null + * @psalm-return CalendarInfo|null + * @return array|null */ public function getCalendarById(int $calendarId): ?array { $fields = array_column($this->propertyMap, 0); @@ -669,7 +688,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return null; } - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $components = []; if ($row['components']) { $components = explode(',', $row['components']); @@ -679,7 +698,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0, '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), @@ -717,7 +736,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return null; } - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $subscription = [ 'id' => $row['id'], 'uri' => $row['uri'], @@ -725,7 +744,44 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'source' => $row['source'], 'lastmodified' => $row['lastmodified'], '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', + ]; + + return $this->rowToSubscription($row, $subscription); + } + + public function getSubscriptionByUri(string $principal, string $uri): ?array { + $fields = array_column($this->subscriptionPropertyMap, 0); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'source'; + $fields[] = 'synctoken'; + $fields[] = 'principaluri'; + $fields[] = 'lastmodified'; + + $query = $this->db->getQueryBuilder(); + $query->select($fields) + ->from('calendarsubscriptions') + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) + ->setMaxResults(1); + $stmt = $query->executeQuery(); + + $row = $stmt->fetch(); + $stmt->closeCursor(); + if ($row === false) { + return null; + } + + $row['principaluri'] = (string)$row['principaluri']; + $subscription = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + 'source' => $row['source'], + 'lastmodified' => $row['lastmodified'], + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', ]; return $this->rowToSubscription($row, $subscription); @@ -754,7 +810,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'uri' => $calendarUri, 'synctoken' => 1, 'transparent' => 0, - 'components' => 'VEVENT,VTODO', + 'components' => 'VEVENT,VTODO,VJOURNAL', 'displayname' => $calendarUri ]; @@ -773,7 +829,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; if (isset($properties[$transp])) { - $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent'); + $values['transparent'] = (int)($properties[$transp]->getValue() === 'transparent'); } foreach ($this->propertyMap as $xmlName => [$dbName, $type]) { @@ -826,7 +882,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription switch ($propertyName) { case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp': $fieldName = 'transparent'; - $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent'); + $newValues[$fieldName] = (int)($propertyValue->getValue() === 'transparent'); break; default: $fieldName = $this->propertyMap[$propertyName][0]; @@ -843,7 +899,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); $query->executeStatement(); - $this->addChanges($calendarId, [""], 2); + $this->addChanges($calendarId, [''], 2); $calendarData = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); @@ -863,7 +919,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @return void */ public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) { - $this->atomic(function () use ($calendarId, $forceDeletePermanently) { + $this->atomic(function () use ($calendarId, $forceDeletePermanently): void { // 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. @@ -874,6 +930,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendarData = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); + $this->purgeCalendarInvitations($calendarId); + $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder(); $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable) ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId))) @@ -924,7 +982,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } public function restoreCalendar(int $id): void { - $this->atomic(function () use ($id) { + $this->atomic(function () use ($id): void { $qb = $this->db->getQueryBuilder(); $update = $qb->update('calendars') ->set('deleted_at', $qb->createNamedParameter(null)) @@ -945,6 +1003,81 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** + * Returns all calendar entries as a stream of data + * + * @since 32.0.0 + * + * @return Generator<array> + */ + public function exportCalendar(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR, ?CalendarExportOptions $options = null): Generator { + // extract options + $rangeStart = $options?->getRangeStart(); + $rangeCount = $options?->getRangeCount(); + // construct query + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('calendarobjects') + ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId))) + ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) + ->andWhere($qb->expr()->isNull('deleted_at')); + if ($rangeStart !== null) { + $qb->andWhere($qb->expr()->gt('uid', $qb->createNamedParameter($rangeStart))); + } + if ($rangeCount !== null) { + $qb->setMaxResults($rangeCount); + } + if ($rangeStart !== null || $rangeCount !== null) { + $qb->orderBy('uid', 'ASC'); + } + $rs = $qb->executeQuery(); + // iterate through results + try { + while (($row = $rs->fetch()) !== false) { + yield $row; + } + } finally { + $rs->closeCursor(); + } + } + + /** + * Returns all calendar objects with limited metadata for a calendar + * + * Every item contains an array with the following keys: + * * id - the table row id + * * etag - An arbitrary string + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string. + * * calendardata - The iCalendar-compatible calendar data + * + * @param mixed $calendarId + * @param int $calendarType + * @return array + */ + public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array { + $query = $this->db->getQueryBuilder(); + $query->select(['id','uid', 'etag', 'uri', 'calendardata']) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) + ->andWhere($query->expr()->isNull('deleted_at')); + $stmt = $query->executeQuery(); + + $result = []; + while (($row = $stmt->fetch()) !== false) { + $result[$row['uid']] = [ + 'id' => $row['id'], + 'etag' => $row['etag'], + 'uri' => $row['uri'], + 'calendardata' => $row['calendardata'], + ]; + } + $stmt->closeCursor(); + + return $result; + } + + /** * Delete all of an user's shares * * @param string $principaluri @@ -1029,12 +1162,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'uri' => $row['uri'], 'lastmodified' => $row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', - 'calendarid' => (int) $row['calendarid'], - 'calendartype' => (int) $row['calendartype'], - 'size' => (int) $row['size'], + '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'] === null ? $row['deleted_at'] : (int) $row['deleted_at'], + 'classification' => (int)$row['classification'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'], ]; } $stmt->closeCursor(); @@ -1073,7 +1206,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'size' => (int)$row['size'], 'component' => strtolower($row['componenttype']), 'classification' => (int)$row['classification'], - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'], ]; } $stmt->closeCursor(); @@ -1104,7 +1237,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $this->cachedObjects[$key]; } $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) + $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) ->from('calendarobjects') ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) @@ -1126,6 +1259,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return [ 'id' => $row['id'], 'uri' => $row['uri'], + 'uid' => $row['uid'], 'lastmodified' => $row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', 'calendarid' => $row['calendarid'], @@ -1133,7 +1267,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'calendardata' => $this->readBlob($row['calendardata']), 'component' => strtolower($row['componenttype']), 'classification' => (int)$row['classification'], - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'], ]; } @@ -1222,7 +1356,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) ->andWhere($qb->expr()->isNull('deleted_at')); $result = $qb->executeQuery(); - $count = (int) $result->fetchOne(); + $count = (int)$result->fetchOne(); $result->closeCursor(); if ($count !== 0) { @@ -1310,15 +1444,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) { $query = $this->db->getQueryBuilder(); $query->update('calendarobjects') - ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) - ->set('lastmodified', $query->createNamedParameter(time())) - ->set('etag', $query->createNamedParameter($extraData['etag'])) - ->set('size', $query->createNamedParameter($extraData['size'])) - ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) - ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) - ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) - ->set('classification', $query->createNamedParameter($extraData['classification'])) - ->set('uid', $query->createNamedParameter($extraData['uid'])) + ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) + ->set('lastmodified', $query->createNamedParameter(time())) + ->set('etag', $query->createNamedParameter($extraData['etag'])) + ->set('size', $query->createNamedParameter($extraData['size'])) + ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) + ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) + ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) + ->set('classification', $query->createNamedParameter($extraData['classification'])) + ->set('uid', $query->createNamedParameter($extraData['uid'])) ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) @@ -1348,37 +1482,40 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** * Moves a calendar object from calendar to calendar. * - * @param int $sourceCalendarId + * @param string $sourcePrincipalUri + * @param int $sourceObjectId + * @param string $targetPrincipalUri * @param int $targetCalendarId - * @param int $objectId - * @param string $oldPrincipalUri - * @param string $newPrincipalUri + * @param string $tragetObjectUri * @param int $calendarType * @return bool * @throws Exception */ - public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool { + public function moveCalendarObject(string $sourcePrincipalUri, int $sourceObjectId, string $targetPrincipalUri, int $targetCalendarId, string $tragetObjectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool { $this->cachedObjects = []; - return $this->atomic(function () use ($sourceCalendarId, $targetCalendarId, $objectId, $oldPrincipalUri, $newPrincipalUri, $calendarType) { - $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId); + return $this->atomic(function () use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) { + $object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId); if (empty($object)) { return false; } + $sourceCalendarId = $object['calendarid']; + $sourceObjectUri = $object['uri']; + $query = $this->db->getQueryBuilder(); $query->update('calendarobjects') ->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR)) + ->where($query->expr()->eq('id', $query->createNamedParameter($sourceObjectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) ->executeStatement(); - $this->purgeProperties($sourceCalendarId, $objectId); - $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType); + $this->purgeProperties($sourceCalendarId, $sourceObjectId); + $this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType); - $this->addChanges($sourceCalendarId, [$object['uri']], 3, $calendarType); - $this->addChanges($targetCalendarId, [$object['uri']], 1, $calendarType); + $this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType); + $this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType); - $object = $this->getCalendarObjectById($newPrincipalUri, $objectId); + $object = $this->getCalendarObjectById($targetPrincipalUri, $sourceObjectId); // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client if (empty($object)) { return false; @@ -1400,25 +1537,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription }, $this->db); } - - /** - * @param int $calendarObjectId - * @param int $classification - */ - public function setClassification($calendarObjectId, $classification) { - $this->cachedObjects = []; - if (!in_array($classification, [ - self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL - ])) { - throw new \InvalidArgumentException(); - } - $query = $this->db->getQueryBuilder(); - $query->update('calendarobjects') - ->set('classification', $query->createNamedParameter($classification)) - ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId))) - ->executeStatement(); - } - /** * Deletes an existing calendar object. * @@ -1432,7 +1550,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) { $this->cachedObjects = []; - $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently) { + $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void { $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType); if ($data === null) { @@ -1446,6 +1564,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->purgeProperties($calendarId, $data['id']); + $this->purgeObjectInvitations($data['uid']); + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { $calendarRow = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); @@ -1461,13 +1581,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription if (!empty($pathInfo['extension'])) { // Append a suffix to "free" the old URI for recreation $newUri = sprintf( - "%s-deleted.%s", + '%s-deleted.%s', $pathInfo['filename'], $pathInfo['extension'] ); } else { $newUri = sprintf( - "%s-deleted", + '%s-deleted', $pathInfo['filename'] ); } @@ -1514,9 +1634,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ public function restoreCalendarObject(array $objectData): void { $this->cachedObjects = []; - $this->atomic(function () use ($objectData) { - $id = (int) $objectData['id']; - $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']); + $this->atomic(function () use ($objectData): void { + $id = (int)$objectData['id']; + $restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']); $targetObject = $this->getCalendarObject( $objectData['calendarid'], $restoreUri @@ -1545,17 +1665,17 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription // Welp, this should possibly not have happened, but let's ignore return; } - $this->addChanges($row['calendarid'], [$row['uri']], 1, (int) $row['calendartype']); + $this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']); - $calendarRow = $this->getCalendarById((int) $row['calendarid']); + $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'], + (int)$objectData['calendarid'], $calendarRow, - $this->getShares((int) $row['calendarid']), + $this->getShares((int)$row['calendarid']), $row ) ); @@ -1642,7 +1762,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } } $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) + $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) ->from('calendarobjects') ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) @@ -1674,13 +1794,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription try { $matches = $this->validateFilterForObject($row, $filters); } catch (ParseException $ex) { - $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [ + $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [ 'app' => 'dav', 'exception' => $ex, ]); continue; } catch (InvalidDataException $ex) { - $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [ + $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [ + 'app' => 'dav', + 'exception' => $ex, + ]); + continue; + } catch (MaxInstancesExceededException $ex) { + $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [ 'app' => 'dav', 'exception' => $ex, ]); @@ -1803,7 +1929,7 @@ 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) { @@ -1845,7 +1971,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription array $searchProperties, array $options, $limit, - $offset + $offset, ) { $outerQuery = $this->db->getQueryBuilder(); $innerQuery = $this->db->getQueryBuilder(); @@ -1874,18 +2000,18 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } if (!empty($searchProperties)) { - $or = $innerQuery->expr()->orX(); + $or = []; foreach ($searchProperties as $searchProperty) { - $or->add($innerQuery->expr()->eq('op.name', - $outerQuery->createNamedParameter($searchProperty))); + $or[] = $innerQuery->expr()->eq('op.name', + $outerQuery->createNamedParameter($searchProperty)); } - $innerQuery->andWhere($or); + $innerQuery->andWhere($innerQuery->expr()->orX(...$or)); } if ($pattern !== '') { $innerQuery->andWhere($innerQuery->expr()->iLike('op.value', - $outerQuery->createNamedParameter('%' . - $this->db->escapeLikeParameter($pattern) . '%'))); + $outerQuery->createNamedParameter('%' + . $this->db->escapeLikeParameter($pattern) . '%'))); } $start = null; @@ -1923,12 +2049,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } if (!empty($options['types'])) { - $or = $outerQuery->expr()->orX(); + $or = []; foreach ($options['types'] as $type) { - $or->add($outerQuery->expr()->eq('componenttype', - $outerQuery->createNamedParameter($type))); + $or[] = $outerQuery->expr()->eq('componenttype', + $outerQuery->createNamedParameter($type)); } - $outerQuery->andWhere($or); + $outerQuery->andWhere($outerQuery->expr()->orX(...$or)); } $outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL()))); @@ -2021,7 +2147,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $calendarObjects; } - private function searchCalendarObjects(IQueryBuilder $query, DateTimeInterface|null $start, DateTimeInterface|null $end): array { + private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface $start, ?DateTimeInterface $end): array { $calendarObjects = []; $filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface); @@ -2034,24 +2160,32 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription continue; } - $isValid = $this->validateFilterForObject($row, [ - 'name' => 'VCALENDAR', - 'comp-filters' => [ - [ - 'name' => 'VEVENT', - 'comp-filters' => [], - 'prop-filters' => [], - 'is-not-defined' => false, - 'time-range' => [ - 'start' => $start, - 'end' => $end, + try { + $isValid = $this->validateFilterForObject($row, [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], ], ], - ], - 'prop-filters' => [], - 'is-not-defined' => false, - 'time-range' => null, - ]); + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + } catch (MaxInstancesExceededException $ex) { + $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [ + 'app' => 'dav', + 'exception' => $ex, + ]); + continue; + } if (is_resource($row['calendardata'])) { // Put the stream back to the beginning so it can be read another time @@ -2143,22 +2277,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription array $componentTypes, array $searchProperties, array $searchParameters, - array $options = [] + array $options = [], ): array { return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) { $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false; $calendarObjectIdQuery = $this->db->getQueryBuilder(); - $calendarOr = $calendarObjectIdQuery->expr()->orX(); - $searchOr = $calendarObjectIdQuery->expr()->orX(); + $calendarOr = []; + $searchOr = []; // Fetch calendars and subscription $calendars = $this->getCalendarsForUser($principalUri); $subscriptions = $this->getSubscriptionsForUser($principalUri); foreach ($calendars as $calendar) { - $calendarAnd = $calendarObjectIdQuery->expr()->andX(); - $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id']))); - $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); + $calendarAnd = $calendarObjectIdQuery->expr()->andX( + $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])), + $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)), + ); // If it's shared, limit search to public events if (isset($calendar['{http://owncloud.org/ns}owner-principal']) @@ -2166,12 +2301,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); } - $calendarOr->add($calendarAnd); + $calendarOr[] = $calendarAnd; } foreach ($subscriptions as $subscription) { - $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(); - $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id']))); - $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); + $subscriptionAnd = $calendarObjectIdQuery->expr()->andX( + $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])), + $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)), + ); // If it's shared, limit search to public events if (isset($subscription['{http://owncloud.org/ns}owner-principal']) @@ -2179,28 +2315,30 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); } - $calendarOr->add($subscriptionAnd); + $calendarOr[] = $subscriptionAnd; } foreach ($searchProperties as $property) { - $propertyAnd = $calendarObjectIdQuery->expr()->andX(); - $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); - $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter')); + $propertyAnd = $calendarObjectIdQuery->expr()->andX( + $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)), + $calendarObjectIdQuery->expr()->isNull('cob.parameter'), + ); - $searchOr->add($propertyAnd); + $searchOr[] = $propertyAnd; } foreach ($searchParameters as $property => $parameter) { - $parameterAnd = $calendarObjectIdQuery->expr()->andX(); - $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); - $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY))); + $parameterAnd = $calendarObjectIdQuery->expr()->andX( + $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)), + $calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)), + ); - $searchOr->add($parameterAnd); + $searchOr[] = $parameterAnd; } - if ($calendarOr->count() === 0) { + if (empty($calendarOr)) { return []; } - if ($searchOr->count() === 0) { + if (empty($searchOr)) { return []; } @@ -2208,8 +2346,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->from($this->dbObjectPropertiesTable, 'cob') ->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($calendarObjectIdQuery->expr()->orX(...$calendarOr)) + ->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr)) ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at')); if ($pattern !== '') { @@ -2244,7 +2382,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $result = $calendarObjectIdQuery->executeQuery(); $matches = []; while (($row = $result->fetch()) !== false) { - $matches[] = (int) $row['objectid']; + $matches[] = (int)$row['objectid']; } $result->closeCursor(); @@ -2331,7 +2469,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'calendardata' => $this->readBlob($row['calendardata']), 'component' => strtolower($row['componenttype']), 'classification' => (int)$row['classification'], - 'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null, + 'deleted_at' => isset($row['deleted_at']) ? ((int)$row['deleted_at']) : null, ]; } @@ -2405,74 +2543,57 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ); $stmt = $qb->executeQuery(); $currentToken = $stmt->fetchOne(); + $initialSync = !is_numeric($syncToken); if ($currentToken === false) { return null; } - $result = [ - 'syncToken' => $currentToken, - 'added' => [], - 'modified' => [], - 'deleted' => [], - ]; - - if ($syncToken) { - $qb = $this->db->getQueryBuilder(); - - $qb->select('uri', 'operation') - ->from('calendarchanges') - ->where( - $qb->expr()->andX( - $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)), - $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)), - $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), - $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)) - ) - )->orderBy('synctoken'); - if (is_int($limit) && $limit > 0) { - $qb->setMaxResults($limit); - } - - // Fetching all changes - $stmt = $qb->executeQuery(); - $changes = []; - - // This loop ensures that any duplicates are overwritten, only the - // last change on a node is relevant. - while ($row = $stmt->fetch()) { - $changes[$row['uri']] = $row['operation']; - } - $stmt->closeCursor(); - - foreach ($changes as $uri => $operation) { - switch ($operation) { - case 1: - $result['added'][] = $uri; - break; - case 2: - $result['modified'][] = $uri; - break; - case 3: - $result['deleted'][] = $uri; - break; - } - } - } else { - // No synctoken supplied, this is the initial sync. + // evaluate if this is a initial sync and construct appropriate command + if ($initialSync) { $qb = $this->db->getQueryBuilder(); $qb->select('uri') ->from('calendarobjects') - ->where( - $qb->expr()->andX( - $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), - $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)) - ) - ); - $stmt = $qb->executeQuery(); + ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId))) + ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) + ->andWhere($qb->expr()->isNull('deleted_at')); + } else { + $qb = $this->db->getQueryBuilder(); + $qb->select('uri', $qb->func()->max('operation')) + ->from('calendarchanges') + ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId))) + ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) + ->andWhere($qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken))) + ->andWhere($qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken))) + ->groupBy('uri'); + } + // evaluate if limit exists + if (is_numeric($limit)) { + $qb->setMaxResults($limit); + } + // execute command + $stmt = $qb->executeQuery(); + // build results + $result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []]; + // retrieve results + if ($initialSync) { $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); - $stmt->closeCursor(); + } else { + // \PDO::FETCH_NUM is needed due to the inconsistent field names + // produced by doctrine for MAX() with different databases + while ($entry = $stmt->fetch(\PDO::FETCH_NUM)) { + // assign uri (column 0) to appropriate mutation based on operation (column 1) + // forced (int) is needed as doctrine with OCI returns the operation field as string not integer + match ((int)$entry[1]) { + 1 => $result['added'][] = $entry[0], + 2 => $result['modified'][] = $entry[0], + 3 => $result['deleted'][] = $entry[0], + default => $this->logger->debug('Unknown calendar change operation detected') + }; + } } + $stmt->closeCursor(); + return $result; }, $this->db); } @@ -2535,7 +2656,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'lastmodified' => $row['lastmodified'], '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', ]; $subscriptions[] = $this->rowToSubscription($row, $subscription); @@ -2657,7 +2778,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @return void */ public function deleteSubscription($subscriptionId) { - $this->atomic(function () use ($subscriptionId) { + $this->atomic(function () use ($subscriptionId): void { $subscriptionRow = $this->getSubscriptionById($subscriptionId); $query = $this->db->getQueryBuilder(); @@ -2740,9 +2861,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function getSchedulingObjects($principalUri) { $query = $this->db->getQueryBuilder(); $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) - ->from('schedulingobjects') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->executeQuery(); + ->from('schedulingobjects') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->executeQuery(); $results = []; while (($row = $stmt->fetch()) !== false) { @@ -2770,9 +2891,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->cachedObjects = []; $query = $this->db->getQueryBuilder(); $query->delete('schedulingobjects') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) - ->executeStatement(); + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->executeStatement(); } /** @@ -2790,7 +2911,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->setMaxResults($limit); $result = $query->executeQuery(); $count = $result->rowCount(); - if($count === 0) { + if ($count === 0) { return; } $ids = array_map(static function (array $id) { @@ -2802,12 +2923,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $deleteQuery = $this->db->getQueryBuilder(); $deleteQuery->delete('schedulingobjects') ->where($deleteQuery->expr()->in('id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)); - foreach(array_chunk($ids, 1000) as $chunk) { + foreach (array_chunk($ids, 1000) as $chunk) { $deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY); $numDeleted += $deleteQuery->executeStatement(); } - if($numDeleted === $limit) { + if ($numDeleted === $limit) { $this->logger->info("Deleted $limit scheduling objects, continuing with next batch"); $this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit); } @@ -2849,7 +2970,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->cachedObjects = []; $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions'; - $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table) { + $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void { $query = $this->db->getQueryBuilder(); $query->select('synctoken') ->from($table) @@ -2866,7 +2987,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'calendarid' => $query->createNamedParameter($calendarId), 'operation' => $query->createNamedParameter($operation), 'calendartype' => $query->createNamedParameter($calendarType), - 'created_at' => time(), + 'created_at' => $query->createNamedParameter(time()), ]); foreach ($objectUris as $uri) { $query->setParameter('uri', $uri); @@ -2884,7 +3005,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void { $this->cachedObjects = []; - $this->atomic(function () use ($calendarId, $calendarType) { + $this->atomic(function () use ($calendarId, $calendarType): void { $qbAdded = $this->db->getQueryBuilder(); $qbAdded->select('uri') ->from('calendarobjects') @@ -2915,7 +3036,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ); $resultDeleted = $qbDeleted->executeQuery(); $deletedUris = array_map(function (string $uri) { - return str_replace("-deleted.ics", ".ics", $uri); + return str_replace('-deleted.ics', '.ics', $uri); }, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN)); $resultDeleted->closeCursor(); $this->addChanges($calendarId, $deletedUris, 3, $calendarType); @@ -2987,7 +3108,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $lastOccurrence = $firstOccurrence; } } else { - $it = new EventIterator($vEvents); + try { + $it = new EventIterator($vEvents); + } catch (NoInstancesException $e) { + $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [ + 'app' => 'dav', + 'exception' => $e, + ]); + throw new Forbidden($e->getMessage()); + } $maxDate = new DateTime(self::MAX_DATE); $firstOccurrence = $it->getDtStart()->getTimestamp(); if ($it->isInfinite()) { @@ -3042,7 +3171,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param list<string> $remove */ public function updateShares(IShareable $shareable, array $add, array $remove): void { - $this->atomic(function () use ($shareable, $add, $remove) { + $this->atomic(function () use ($shareable, $add, $remove): void { $calendarId = $shareable->getResourceId(); $calendarRow = $this->getCalendarById($calendarId); if ($calendarRow === null) { @@ -3069,7 +3198,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** * @param boolean $value - * @param \OCA\DAV\CalDAV\Calendar $calendar + * @param Calendar $calendar * @return string|null */ public function setPublishStatus($value, $calendar) { @@ -3104,7 +3233,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * @param \OCA\DAV\CalDAV\Calendar $calendar + * @param Calendar $calendar * @return mixed */ public function getPublishStatus($calendar) { @@ -3140,7 +3269,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { $this->cachedObjects = []; - $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType) { + $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void { $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType); try { @@ -3181,7 +3310,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $query->setParameter('name', $property->name); $query->setParameter('parameter', null); - $query->setParameter('value', $value); + $query->setParameter('value', mb_strcut($value, 0, 254)); $query->executeStatement(); } @@ -3212,7 +3341,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * deletes all birthday calendars */ public function deleteAllBirthdayCalendars() { - $this->atomic(function () { + $this->atomic(function (): void { $query = $this->db->getQueryBuilder(); $result = $query->select(['id'])->from('calendars') ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))) @@ -3232,7 +3361,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param $subscriptionId */ public function purgeAllCachedEventsForSubscription($subscriptionId) { - $this->atomic(function () use ($subscriptionId) { + $this->atomic(function () use ($subscriptionId): void { $query = $this->db->getQueryBuilder(); $query->select('uri') ->from('calendarobjects') @@ -3269,6 +3398,45 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** + * @param int $subscriptionId + * @param array<int> $calendarObjectIds + * @param array<string> $calendarObjectUris + */ + public function purgeCachedEventsForSubscription(int $subscriptionId, array $calendarObjectIds, array $calendarObjectUris): void { + if (empty($calendarObjectUris)) { + return; + } + + $this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void { + foreach (array_chunk($calendarObjectIds, 1000) as $chunk) { + $query = $this->db->getQueryBuilder(); + $query->delete($this->dbObjectPropertiesTable) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY)) + ->executeStatement(); + + $query = $this->db->getQueryBuilder(); + $query->delete('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY)) + ->executeStatement(); + } + + foreach (array_chunk($calendarObjectUris, 1000) as $chunk) { + $query = $this->db->getQueryBuilder(); + $query->delete('calendarchanges') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->andWhere($query->expr()->in('uri', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY)) + ->executeStatement(); + } + $this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION); + }, $this->db); + } + + /** * Move a calendar from one user to another * * @param string $uriName @@ -3351,7 +3519,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->from('calendarchanges'); $result = $query->executeQuery(); - $maxId = (int) $result->fetchOne(); + $maxId = (int)$result->fetchOne(); $result->closeCursor(); if (!$maxId || $maxId < $keep) { return 0; @@ -3455,4 +3623,68 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } return $subscription; } + + /** + * delete all invitations from a given calendar + * + * @since 31.0.0 + * + * @param int $calendarId + * + * @return void + */ + protected function purgeCalendarInvitations(int $calendarId): void { + // select all calendar object uid's + $cmd = $this->db->getQueryBuilder(); + $cmd->select('uid') + ->from($this->dbObjectsTable) + ->where($cmd->expr()->eq('calendarid', $cmd->createNamedParameter($calendarId))); + $allIds = $cmd->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + // delete all links that match object uid's + $cmd = $this->db->getQueryBuilder(); + $cmd->delete($this->dbObjectInvitationsTable) + ->where($cmd->expr()->in('uid', $cmd->createParameter('uids'), IQueryBuilder::PARAM_STR_ARRAY)); + foreach (array_chunk($allIds, 1000) as $chunkIds) { + $cmd->setParameter('uids', $chunkIds, IQueryBuilder::PARAM_STR_ARRAY); + $cmd->executeStatement(); + } + } + + /** + * Delete all invitations from a given calendar event + * + * @since 31.0.0 + * + * @param string $eventId UID of the event + * + * @return void + */ + protected function purgeObjectInvitations(string $eventId): void { + $cmd = $this->db->getQueryBuilder(); + $cmd->delete($this->dbObjectInvitationsTable) + ->where($cmd->expr()->eq('uid', $cmd->createNamedParameter($eventId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)); + $cmd->executeStatement(); + } + + public function unshare(IShareable $shareable, string $principal): void { + $this->atomic(function () use ($shareable, $principal): void { + $calendarData = $this->getCalendarById($shareable->getResourceId()); + if ($calendarData === null) { + throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $shareable->getResourceId()); + } + + $oldShares = $this->getShares($shareable->getResourceId()); + $unshare = $this->calendarSharingBackend->unshare($shareable, $principal); + + if ($unshare) { + $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent( + $shareable->getResourceId(), + $calendarData, + $oldShares, + [], + [$principal] + )); + } + }, $this->db); + } } |