diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index d11aaa06a1a0c..090d35111eb90 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -315,132 +315,134 @@ public function getDeletedCalendars(int $deletedBefore): array { * @return array */ public function getCalendarsForUser($principalUri) { - $principalUriOriginal = $principalUri; - $principalUri = $this->convertPrincipal($principalUri, true); - $fields = array_column($this->propertyMap, 0); - $fields[] = 'id'; - $fields[] = 'uri'; - $fields[] = 'synctoken'; - $fields[] = 'components'; - $fields[] = 'principaluri'; - $fields[] = 'transparent'; - - // Making fields a comma-delimited list - $query = $this->db->getQueryBuilder(); - $query->select($fields) - ->from('calendars') - ->orderBy('calendarorder', 'ASC'); + return $this->atomic(function () use ($principalUri) { + $principalUriOriginal = $principalUri; + $principalUri = $this->convertPrincipal($principalUri, true); + $fields = array_column($this->propertyMap, 0); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + + // Making fields a comma-delimited list + $query = $this->db->getQueryBuilder(); + $query->select($fields) + ->from('calendars') + ->orderBy('calendarorder', 'ASC'); - if ($principalUri === '') { - $query->where($query->expr()->emptyString('principaluri')); - } else { - $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); - } + if ($principalUri === '') { + $query->where($query->expr()->emptyString('principaluri')); + } else { + $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); + } - $result = $query->executeQuery(); + $result = $query->executeQuery(); - $calendars = []; - while ($row = $result->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; - $components = []; - if ($row['components']) { - $components = explode(',', $row['components']); - } + $calendars = []; + while ($row = $result->fetch()) { + $row['principaluri'] = (string) $row['principaluri']; + $components = []; + if ($row['components']) { + $components = explode(',', $row['components']); + } - $calendar = [ - '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_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), - ]; + $calendar = [ + '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_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), + ]; - $calendar = $this->rowToCalendar($row, $calendar); - $calendar = $this->addOwnerPrincipalToCalendar($calendar); - $calendar = $this->addResourceTypeToCalendar($row, $calendar); + $calendar = $this->rowToCalendar($row, $calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); - if (!isset($calendars[$calendar['id']])) { - $calendars[$calendar['id']] = $calendar; + if (!isset($calendars[$calendar['id']])) { + $calendars[$calendar['id']] = $calendar; + } } - } - $result->closeCursor(); + $result->closeCursor(); - // query for shared calendars - $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); - $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); + // query for shared calendars + $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); + $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); - $principals[] = $principalUri; + $principals[] = $principalUri; - $fields = array_column($this->propertyMap, 0); - $fields[] = 'a.id'; - $fields[] = 'a.uri'; - $fields[] = 'a.synctoken'; - $fields[] = 'a.components'; - $fields[] = 'a.principaluri'; - $fields[] = 'a.transparent'; - $fields[] = 's.access'; - $query = $this->db->getQueryBuilder(); - $query->select($fields) - ->from('dav_shares', 's') - ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) - ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) - ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) - ->setParameter('type', 'calendar') - ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); - - $result = $query->executeQuery(); + $fields = array_column($this->propertyMap, 0); + $fields[] = 'a.id'; + $fields[] = 'a.uri'; + $fields[] = 'a.synctoken'; + $fields[] = 'a.components'; + $fields[] = 'a.principaluri'; + $fields[] = 'a.transparent'; + $fields[] = 's.access'; + $query = $this->db->getQueryBuilder(); + $query->select($fields) + ->from('dav_shares', 's') + ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) + ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) + ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) + ->setParameter('type', 'calendar') + ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); - $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; - while ($row = $result->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; - if ($row['principaluri'] === $principalUri) { - continue; - } + $result = $query->executeQuery(); - $readOnly = (int) $row['access'] === Backend::ACCESS_READ; - if (isset($calendars[$row['id']])) { - if ($readOnly) { - // New share can not have more permissions then the old one. + $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; + while ($row = $result->fetch()) { + $row['principaluri'] = (string) $row['principaluri']; + if ($row['principaluri'] === $principalUri) { continue; } - if (isset($calendars[$row['id']][$readOnlyPropertyName]) && - $calendars[$row['id']][$readOnlyPropertyName] === 0) { - // Old share is already read-write, no more permissions can be gained - continue; + + $readOnly = (int) $row['access'] === Backend::ACCESS_READ; + if (isset($calendars[$row['id']])) { + if ($readOnly) { + // New share can not have more permissions then the old one. + continue; + } + if (isset($calendars[$row['id']][$readOnlyPropertyName]) && + $calendars[$row['id']][$readOnlyPropertyName] === 0) { + // Old share is already read-write, no more permissions can be gained + continue; + } } - } - [, $name] = Uri\split($row['principaluri']); - $uri = $row['uri'] . '_shared_by_' . $name; - $row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')'; - $components = []; - if ($row['components']) { - $components = explode(',', $row['components']); - } - $calendar = [ - '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_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), - $readOnlyPropertyName => $readOnly, - ]; + [, $name] = Uri\split($row['principaluri']); + $uri = $row['uri'] . '_shared_by_' . $name; + $row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')'; + $components = []; + if ($row['components']) { + $components = explode(',', $row['components']); + } + $calendar = [ + '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_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), + $readOnlyPropertyName => $readOnly, + ]; - $calendar = $this->rowToCalendar($row, $calendar); - $calendar = $this->addOwnerPrincipalToCalendar($calendar); - $calendar = $this->addResourceTypeToCalendar($row, $calendar); + $calendar = $this->rowToCalendar($row, $calendar); + $calendar = $this->addOwnerPrincipalToCalendar($calendar); + $calendar = $this->addResourceTypeToCalendar($row, $calendar); - $calendars[$calendar['id']] = $calendar; - } - $result->closeCursor(); + $calendars[$calendar['id']] = $calendar; + } + $result->closeCursor(); - return array_values($calendars); + return array_values($calendars); + }, $this->db); } /** @@ -830,39 +832,41 @@ public function createCalendar($principalUri, $calendarUri, array $properties) { * @return void */ public function updateCalendar($calendarId, PropPatch $propPatch) { - $supportedProperties = array_keys($this->propertyMap); - $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; - - $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) { - $newValues = []; - foreach ($mutations as $propertyName => $propertyValue) { - switch ($propertyName) { - case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp': - $fieldName = 'transparent'; - $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent'); - break; - default: - $fieldName = $this->propertyMap[$propertyName][0]; - $newValues[$fieldName] = $propertyValue; - break; + $this->atomic(function () use ($calendarId, $propPatch) { + $supportedProperties = array_keys($this->propertyMap); + $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; + + $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) { + $newValues = []; + foreach ($mutations as $propertyName => $propertyValue) { + switch ($propertyName) { + case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp': + $fieldName = 'transparent'; + $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent'); + break; + default: + $fieldName = $this->propertyMap[$propertyName][0]; + $newValues[$fieldName] = $propertyValue; + break; + } } - } - $query = $this->db->getQueryBuilder(); - $query->update('calendars'); - foreach ($newValues as $fieldName => $value) { - $query->set($fieldName, $query->createNamedParameter($value)); - } - $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); - $query->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->update('calendars'); + foreach ($newValues as $fieldName => $value) { + $query->set($fieldName, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); + $query->executeStatement(); - $this->addChange($calendarId, "", 2); + $this->addChange($calendarId, "", 2); - $calendarData = $this->getCalendarById($calendarId); - $shares = $this->getShares($calendarId); - $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations)); + $calendarData = $this->getCalendarById($calendarId); + $shares = $this->getShares($calendarId); + $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations)); - return true; - }); + return true; + }); + }, $this->db); } /** @@ -872,81 +876,85 @@ public function updateCalendar($calendarId, PropPatch $propPatch) { * @return void */ 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); - $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) { + $this->atomic(function () use ($calendarId, $forceDeletePermanently) { + // 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); - - $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(); - - $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(); - - $qbDeleteCalendarChanges = $this->db->getQueryBuilder(); - $qbDeleteCalendarChanges->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); - - $qbDeleteCalendar = $this->db->getQueryBuilder(); - $qbDeleteCalendar->delete('calendars') - ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId))) - ->executeStatement(); + $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); - // Only dispatch if we actually deleted anything - if ($calendarData) { - $this->dispatcher->dispatchTyped(new CalendarDeletedEvent($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(); + $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(); + + $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(); + + $qbDeleteCalendarChanges = $this->db->getQueryBuilder(); + $qbDeleteCalendarChanges->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); + + $qbDeleteCalendar = $this->db->getQueryBuilder(); + $qbDeleteCalendar->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($calendarId, $calendarData, $shares)); + } + } else { + $qbMarkCalendarDeleted = $this->db->getQueryBuilder(); + $qbMarkCalendarDeleted->update('calendars') + ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time())) + ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId))) + ->executeStatement(); - $calendarData = $this->getCalendarById($calendarId); - $shares = $this->getShares($calendarId); - if ($calendarData) { - $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent( - $calendarId, - $calendarData, - $shares - )); + $calendarData = $this->getCalendarById($calendarId); + $shares = $this->getShares($calendarId); + if ($calendarData) { + $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent( + $calendarId, + $calendarData, + $shares + )); + } } - } + }, $this->db); } 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 - )); + $this->atomic(function () use ($id) { + $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 + )); + }, $this->db); } /** @@ -1206,74 +1214,76 @@ public function getMultipleCalendarObjects($calendarId, array $uris, $calendarTy public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { $extraData = $this->getDenormalizedData($calendarData); - // Try to detect duplicates - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->func()->count('*')) - ->from('calendarobjects') - ->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(); + return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) { + // Try to detect duplicates + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*')) + ->from('calendarobjects') + ->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 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('*') - ->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(); - $found = $result->fetch(); - $result->closeCursor(); - if ($found !== false) { - // the object existed previously but has been deleted - // remove the trashbin entry and continue as if it was a new object - $this->deleteCalendarObject($calendarId, $found['uri']); - } + if ($count !== 0) { + 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('*') + ->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(); + $found = $result->fetch(); + $result->closeCursor(); + if ($found !== false) { + // the object existed previously but has been deleted + // remove the trashbin entry and continue as if it was a new object + $this->deleteCalendarObject($calendarId, $found['uri']); + } - $query = $this->db->getQueryBuilder(); - $query->insert('calendarobjects') - ->values([ - 'calendarid' => $query->createNamedParameter($calendarId), - 'uri' => $query->createNamedParameter($objectUri), - 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB), - 'lastmodified' => $query->createNamedParameter(time()), - 'etag' => $query->createNamedParameter($extraData['etag']), - 'size' => $query->createNamedParameter($extraData['size']), - 'componenttype' => $query->createNamedParameter($extraData['componentType']), - 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']), - 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']), - 'classification' => $query->createNamedParameter($extraData['classification']), - 'uid' => $query->createNamedParameter($extraData['uid']), - 'calendartype' => $query->createNamedParameter($calendarType), - ]) - ->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'calendarid' => $query->createNamedParameter($calendarId), + 'uri' => $query->createNamedParameter($objectUri), + 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB), + 'lastmodified' => $query->createNamedParameter(time()), + 'etag' => $query->createNamedParameter($extraData['etag']), + 'size' => $query->createNamedParameter($extraData['size']), + 'componenttype' => $query->createNamedParameter($extraData['componentType']), + 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']), + 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']), + 'classification' => $query->createNamedParameter($extraData['classification']), + 'uid' => $query->createNamedParameter($extraData['uid']), + 'calendartype' => $query->createNamedParameter($calendarType), + ]) + ->executeStatement(); - $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType); - $this->addChange($calendarId, $objectUri, 1, $calendarType); + $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType); + $this->addChange($calendarId, $objectUri, 1, $calendarType); - $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); - assert($objectRow !== null); + $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); + assert($objectRow !== null); - if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { - $calendarRow = $this->getCalendarById($calendarId); - $shares = $this->getShares($calendarId); + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { + $calendarRow = $this->getCalendarById($calendarId); + $shares = $this->getShares($calendarId); - $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow)); - } else { - $subscriptionRow = $this->getSubscriptionById($calendarId); + $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow)); + } else { + $subscriptionRow = $this->getSubscriptionById($calendarId); - $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow)); - } + $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow)); + } - return '"' . $extraData['etag'] . '"'; + return '"' . $extraData['etag'] . '"'; + }, $this->db); } /** @@ -1297,40 +1307,43 @@ public function createCalendarObject($calendarId, $objectUri, $calendarData, $ca */ public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { $extraData = $this->getDenormalizedData($calendarData); - $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'])) - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) - ->executeStatement(); - $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType); - $this->addChange($calendarId, $objectUri, 2, $calendarType); + 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'])) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) + ->executeStatement(); - $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); - if (is_array($objectRow)) { - if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { - $calendarRow = $this->getCalendarById($calendarId); - $shares = $this->getShares($calendarId); + $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType); + $this->addChange($calendarId, $objectUri, 2, $calendarType); - $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow)); - } else { - $subscriptionRow = $this->getSubscriptionById($calendarId); + $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); + if (is_array($objectRow)) { + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { + $calendarRow = $this->getCalendarById($calendarId); + $shares = $this->getShares($calendarId); - $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow)); + $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow)); + } else { + $subscriptionRow = $this->getSubscriptionById($calendarId); + + $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow)); + } } - } - return '"' . $extraData['etag'] . '"'; + return '"' . $extraData['etag'] . '"'; + }, $this->db); } /** @@ -1346,43 +1359,45 @@ public function updateCalendarObject($calendarId, $objectUri, $calendarData, $ca * @throws Exception */ public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool { - $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId); - if (empty($object)) { - return false; - } + return $this->atomic(function () use ($sourceCalendarId, $targetCalendarId, $objectId, $oldPrincipalUri, $newPrincipalUri, $calendarType) { + $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId); + if (empty($object)) { + return false; + } - $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)) - ->executeStatement(); + $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)) + ->executeStatement(); - $this->purgeProperties($sourceCalendarId, $objectId); - $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType); + $this->purgeProperties($sourceCalendarId, $objectId); + $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType); - $this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType); - $this->addChange($targetCalendarId, $object['uri'], 3, $calendarType); + $this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType); + $this->addChange($targetCalendarId, $object['uri'], 3, $calendarType); - $object = $this->getCalendarObjectById($newPrincipalUri, $objectId); - // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client - if (empty($object)) { - return false; - } + $object = $this->getCalendarObjectById($newPrincipalUri, $objectId); + // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client + if (empty($object)) { + return false; + } - $targetCalendarRow = $this->getCalendarById($targetCalendarId); - // the calendar this event is being moved to does not exist any longer - if (empty($targetCalendarRow)) { - return false; - } + $targetCalendarRow = $this->getCalendarById($targetCalendarId); + // the calendar this event is being moved to does not exist any longer + if (empty($targetCalendarRow)) { + return false; + } - if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { - $sourceShares = $this->getShares($sourceCalendarId); - $targetShares = $this->getShares($targetCalendarId); - $sourceCalendarRow = $this->getCalendarById($sourceCalendarId); - $this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object)); - } - return true; + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { + $sourceShares = $this->getShares($sourceCalendarId); + $targetShares = $this->getShares($targetCalendarId); + $sourceCalendarRow = $this->getCalendarById($sourceCalendarId); + $this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object)); + } + return true; + }, $this->db); } @@ -1415,77 +1430,79 @@ public function setClassification($calendarObjectId, $classification) { * @return void */ public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) { - $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType); + $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently) { + $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType); - if ($data === null) { - // Nothing to delete - return; - } + 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]); + 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']); + $this->purgeProperties($calendarId, $data['id']); - if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { - $calendarRow = $this->getCalendarById($calendarId); - $shares = $this->getShares($calendarId); + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { + $calendarRow = $this->getCalendarById($calendarId); + $shares = $this->getShares($calendarId); - $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data)); - } else { - $subscriptionRow = $this->getSubscriptionById($calendarId); + $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data)); + } else { + $subscriptionRow = $this->getSubscriptionById($calendarId); - $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data)); - } - } 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'] - ); + $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data)); + } } else { - $newUri = sprintf( - "%s-deleted", - $pathInfo['filename'] - ); - } - - // 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"); - } + $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'] + ); + } - $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(); + // 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"); + } - $calendarData = $this->getCalendarById($calendarId); - if ($calendarData !== null) { - $this->dispatcher->dispatchTyped( - new CalendarObjectMovedToTrashEvent( - $calendarId, - $calendarData, - $this->getShares($calendarId), - $data - ) - ); + $qb = $this->db->getQueryBuilder(); + $markObjectDeletedQuery = $qb->update('calendarobjects') + ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT)) + ->set('uri', $qb->createNamedParameter($newUri)) + ->where( + $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), + $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), + $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri)) + ); + $markObjectDeletedQuery->executeStatement(); + + $calendarData = $this->getCalendarById($calendarId); + if ($calendarData !== null) { + $this->dispatcher->dispatchTyped( + new CalendarObjectMovedToTrashEvent( + $calendarId, + $calendarData, + $this->getShares($calendarId), + $data + ) + ); + } } - } - $this->addChange($calendarId, $objectUri, 3, $calendarType); + $this->addChange($calendarId, $objectUri, 3, $calendarType); + }, $this->db); } /** @@ -1494,50 +1511,52 @@ public function deleteCalendarObject($calendarId, $objectUri, $calendarType = se * @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"); - } + $this->atomic(function () use ($objectData) { + $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') - ->selectAlias('componenttype', 'component') - ->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']); + $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') + ->selectAlias('componenttype', 'component') + ->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 - ) - ); + $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 + ) + ); + }, $this->db); } /** @@ -1686,118 +1705,120 @@ public function calendarQuery($calendarId, array $filters, $calendarType = self: * @return array */ public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) { - $calendars = $this->getCalendarsForUser($principalUri); - $ownCalendars = []; - $sharedCalendars = []; + return $this->atomic(function () use ($principalUri, $filters, $limit, $offset) { + $calendars = $this->getCalendarsForUser($principalUri); + $ownCalendars = []; + $sharedCalendars = []; - $uriMapper = []; + $uriMapper = []; - foreach ($calendars as $calendar) { - if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) { - $ownCalendars[] = $calendar['id']; - } else { - $sharedCalendars[] = $calendar['id']; + foreach ($calendars as $calendar) { + if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) { + $ownCalendars[] = $calendar['id']; + } else { + $sharedCalendars[] = $calendar['id']; + } + $uriMapper[$calendar['id']] = $calendar['uri']; + } + if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) { + return []; } - $uriMapper[$calendar['id']] = $calendar['uri']; - } - if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) { - return []; - } - - $query = $this->db->getQueryBuilder(); - // Calendar id expressions - $calendarExpressions = []; - foreach ($ownCalendars as $id) { - $calendarExpressions[] = $query->expr()->andX( - $query->expr()->eq('c.calendarid', - $query->createNamedParameter($id)), - $query->expr()->eq('c.calendartype', - $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); - } - foreach ($sharedCalendars as $id) { - $calendarExpressions[] = $query->expr()->andX( - $query->expr()->eq('c.calendarid', - $query->createNamedParameter($id)), - $query->expr()->eq('c.classification', - $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)), - $query->expr()->eq('c.calendartype', - $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); - } - if (count($calendarExpressions) === 1) { - $calExpr = $calendarExpressions[0]; - } else { - $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions); - } + $query = $this->db->getQueryBuilder(); + // Calendar id expressions + $calendarExpressions = []; + foreach ($ownCalendars as $id) { + $calendarExpressions[] = $query->expr()->andX( + $query->expr()->eq('c.calendarid', + $query->createNamedParameter($id)), + $query->expr()->eq('c.calendartype', + $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); + } + foreach ($sharedCalendars as $id) { + $calendarExpressions[] = $query->expr()->andX( + $query->expr()->eq('c.calendarid', + $query->createNamedParameter($id)), + $query->expr()->eq('c.classification', + $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)), + $query->expr()->eq('c.calendartype', + $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); + } - // Component expressions - $compExpressions = []; - foreach ($filters['comps'] as $comp) { - $compExpressions[] = $query->expr() - ->eq('c.componenttype', $query->createNamedParameter($comp)); - } + if (count($calendarExpressions) === 1) { + $calExpr = $calendarExpressions[0]; + } else { + $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions); + } - if (count($compExpressions) === 1) { - $compExpr = $compExpressions[0]; - } else { - $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions); - } + // Component expressions + $compExpressions = []; + foreach ($filters['comps'] as $comp) { + $compExpressions[] = $query->expr() + ->eq('c.componenttype', $query->createNamedParameter($comp)); + } - if (!isset($filters['props'])) { - $filters['props'] = []; - } - if (!isset($filters['params'])) { - $filters['params'] = []; - } + if (count($compExpressions) === 1) { + $compExpr = $compExpressions[0]; + } else { + $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions); + } - $propParamExpressions = []; - foreach ($filters['props'] as $prop) { - $propParamExpressions[] = $query->expr()->andX( - $query->expr()->eq('i.name', $query->createNamedParameter($prop)), - $query->expr()->isNull('i.parameter') - ); - } - foreach ($filters['params'] as $param) { - $propParamExpressions[] = $query->expr()->andX( - $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])), - $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter'])) - ); - } + if (!isset($filters['props'])) { + $filters['props'] = []; + } + if (!isset($filters['params'])) { + $filters['params'] = []; + } - if (count($propParamExpressions) === 1) { - $propParamExpr = $propParamExpressions[0]; - } else { - $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions); - } + $propParamExpressions = []; + foreach ($filters['props'] as $prop) { + $propParamExpressions[] = $query->expr()->andX( + $query->expr()->eq('i.name', $query->createNamedParameter($prop)), + $query->expr()->isNull('i.parameter') + ); + } + foreach ($filters['params'] as $param) { + $propParamExpressions[] = $query->expr()->andX( + $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])), + $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter'])) + ); + } - $query->select(['c.calendarid', 'c.uri']) - ->from($this->dbObjectPropertiesTable, 'i') - ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id')) - ->where($calExpr) - ->andWhere($compExpr) - ->andWhere($propParamExpr) - ->andWhere($query->expr()->iLike('i.value', - $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%'))) - ->andWhere($query->expr()->isNull('deleted_at')); + if (count($propParamExpressions) === 1) { + $propParamExpr = $propParamExpressions[0]; + } else { + $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions); + } - if ($offset) { - $query->setFirstResult($offset); - } - if ($limit) { - $query->setMaxResults($limit); - } + $query->select(['c.calendarid', 'c.uri']) + ->from($this->dbObjectPropertiesTable, 'i') + ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id')) + ->where($calExpr) + ->andWhere($compExpr) + ->andWhere($propParamExpr) + ->andWhere($query->expr()->iLike('i.value', + $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%'))) + ->andWhere($query->expr()->isNull('deleted_at')); + + if ($offset) { + $query->setFirstResult($offset); + } + if ($limit) { + $query->setMaxResults($limit); + } - $stmt = $query->executeQuery(); + $stmt = $query->executeQuery(); - $result = []; - while ($row = $stmt->fetch()) { - $path = $uriMapper[$row['calendarid']] . '/' . $row['uri']; - if (!in_array($path, $result)) { - $result[] = $path; + $result = []; + while ($row = $stmt->fetch()) { + $path = $uriMapper[$row['calendarid']] . '/' . $row['uri']; + if (!in_array($path, $result)) { + $result[] = $path; + } } - } - return $result; + return $result; + }, $this->db); } /** @@ -2022,110 +2043,112 @@ public function searchPrincipalUri(string $principalUri, array $searchProperties, array $searchParameters, array $options = []): array { - $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(); - - // 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))); + 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(); + + // 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))); + + // If it's shared, limit search to public events + if (isset($calendar['{http://owncloud.org/ns}owner-principal']) + && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) { + $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); + } - // If it's shared, limit search to public events - if (isset($calendar['{http://owncloud.org/ns}owner-principal']) - && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) { - $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); + $calendarOr->add($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))); + + // If it's shared, limit search to public events + if (isset($subscription['{http://owncloud.org/ns}owner-principal']) + && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) { + $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); + } - $calendarOr->add($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))); - - // If it's shared, limit search to public events - if (isset($subscription['{http://owncloud.org/ns}owner-principal']) - && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) { - $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); + $calendarOr->add($subscriptionAnd); } - $calendarOr->add($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')); + 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')); - $searchOr->add($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))); + $searchOr->add($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))); - $searchOr->add($parameterAnd); - } + $searchOr->add($parameterAnd); + } - if ($calendarOr->count() === 0) { - return []; - } - if ($searchOr->count() === 0) { - return []; - } + if ($calendarOr->count() === 0) { + return []; + } + if ($searchOr->count() === 0) { + return []; + } - $calendarObjectIdQuery->selectDistinct('cob.objectid') - ->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()->isNull('deleted_at')); - - if ('' !== $pattern) { - if (!$escapePattern) { - $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern))); - } else { - $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); + $calendarObjectIdQuery->selectDistinct('cob.objectid') + ->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()->isNull('deleted_at')); + + if ('' !== $pattern) { + if (!$escapePattern) { + $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern))); + } else { + $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); + } } - } - if (isset($options['limit'])) { - $calendarObjectIdQuery->setMaxResults($options['limit']); - } - if (isset($options['offset'])) { - $calendarObjectIdQuery->setFirstResult($options['offset']); - } + if (isset($options['limit'])) { + $calendarObjectIdQuery->setMaxResults($options['limit']); + } + if (isset($options['offset'])) { + $calendarObjectIdQuery->setFirstResult($options['offset']); + } - $result = $calendarObjectIdQuery->executeQuery(); - $matches = $result->fetchAll(); - $result->closeCursor(); - $matches = array_map(static function (array $match):int { - return (int) $match['objectid']; - }, $matches); + $result = $calendarObjectIdQuery->executeQuery(); + $matches = $result->fetchAll(); + $result->closeCursor(); + $matches = array_map(static function (array $match):int { + return (int) $match['objectid']; + }, $matches); - $query = $this->db->getQueryBuilder(); - $query->select('calendardata', 'uri', 'calendarid', 'calendartype') - ->from('calendarobjects') - ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY))); + $query = $this->db->getQueryBuilder(); + $query->select('calendardata', 'uri', 'calendarid', 'calendartype') + ->from('calendarobjects') + ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY))); - $result = $query->executeQuery(); - $calendarObjects = $result->fetchAll(); - $result->closeCursor(); + $result = $query->executeQuery(); + $calendarObjects = $result->fetchAll(); + $result->closeCursor(); - return array_map(function (array $array): array { - $array['calendarid'] = (int)$array['calendarid']; - $array['calendartype'] = (int)$array['calendartype']; - $array['calendardata'] = $this->readBlob($array['calendardata']); + return array_map(function (array $array): array { + $array['calendarid'] = (int)$array['calendarid']; + $array['calendartype'] = (int)$array['calendartype']; + $array['calendardata'] = $this->readBlob($array['calendardata']); - return $array; - }, $calendarObjects); + return $array; + }, $calendarObjects); + }, $this->db); } /** @@ -2254,84 +2277,86 @@ public function getCalendarObjectById(string $principalUri, int $id): ?array { * @return array */ public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) { - // Current synctoken - $qb = $this->db->getQueryBuilder(); - $qb->select('synctoken') - ->from('calendars') - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($calendarId)) - ); - $stmt = $qb->executeQuery(); - $currentToken = $stmt->fetchOne(); - - if ($currentToken === false) { - return null; - } - - $result = [ - 'syncToken' => $currentToken, - 'added' => [], - 'modified' => [], - 'deleted' => [], - ]; - - if ($syncToken) { + return $this->atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType) { + // Current synctoken $qb = $this->db->getQueryBuilder(); - - $qb->select('uri', 'operation') - ->from('calendarchanges') + $qb->select('synctoken') + ->from('calendars') ->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 + $qb->expr()->eq('id', $qb->createNamedParameter($calendarId)) + ); $stmt = $qb->executeQuery(); - $changes = []; + $currentToken = $stmt->fetchOne(); - // 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']; + if ($currentToken === false) { + return null; } - $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; + $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. + $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(); + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + $stmt->closeCursor(); } - } else { - // No synctoken supplied, this is the initial sync. - $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(); - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); - $stmt->closeCursor(); - } - return $result; + return $result; + }, $this->db); } /** @@ -2473,35 +2498,37 @@ public function createSubscription($principalUri, $uri, array $properties) { * @return void */ public function updateSubscription($subscriptionId, PropPatch $propPatch) { - $supportedProperties = array_keys($this->subscriptionPropertyMap); - $supportedProperties[] = '{http://calendarserver.org/ns/}source'; - - $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) { - $newValues = []; - - foreach ($mutations as $propertyName => $propertyValue) { - if ($propertyName === '{http://calendarserver.org/ns/}source') { - $newValues['source'] = $propertyValue->getHref(); - } else { - $fieldName = $this->subscriptionPropertyMap[$propertyName][0]; - $newValues[$fieldName] = $propertyValue; + $this->atomic(function () use ($subscriptionId, $propPatch) { + $supportedProperties = array_keys($this->subscriptionPropertyMap); + $supportedProperties[] = '{http://calendarserver.org/ns/}source'; + + $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) { + $newValues = []; + + foreach ($mutations as $propertyName => $propertyValue) { + if ($propertyName === '{http://calendarserver.org/ns/}source') { + $newValues['source'] = $propertyValue->getHref(); + } else { + $fieldName = $this->subscriptionPropertyMap[$propertyName][0]; + $newValues[$fieldName] = $propertyValue; + } } - } - $query = $this->db->getQueryBuilder(); - $query->update('calendarsubscriptions') - ->set('lastmodified', $query->createNamedParameter(time())); - foreach ($newValues as $fieldName => $value) { - $query->set($fieldName, $query->createNamedParameter($value)); - } - $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) - ->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->update('calendarsubscriptions') + ->set('lastmodified', $query->createNamedParameter(time())); + foreach ($newValues as $fieldName => $value) { + $query->set($fieldName, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) + ->executeStatement(); - $subscriptionRow = $this->getSubscriptionById($subscriptionId); - $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations)); + $subscriptionRow = $this->getSubscriptionById($subscriptionId); + $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations)); - return true; - }); + return true; + }); + }, $this->db); } /** @@ -2511,32 +2538,34 @@ public function updateSubscription($subscriptionId, PropPatch $propPatch) { * @return void */ public function deleteSubscription($subscriptionId) { - $subscriptionRow = $this->getSubscriptionById($subscriptionId); + $this->atomic(function () use ($subscriptionId) { + $subscriptionRow = $this->getSubscriptionById($subscriptionId); - $query = $this->db->getQueryBuilder(); - $query->delete('calendarsubscriptions') - ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) - ->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->delete('calendarsubscriptions') + ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) + ->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))) - ->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))) + ->executeStatement(); - $query->delete('calendarchanges') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) - ->executeStatement(); + $query->delete('calendarchanges') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->executeStatement(); - $query->delete($this->dbObjectPropertiesTable) - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) - ->executeStatement(); + $query->delete($this->dbObjectPropertiesTable) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->executeStatement(); - if ($subscriptionRow) { - $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, [])); - } + if ($subscriptionRow) { + $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, [])); + } + }, $this->db); } /** @@ -2658,31 +2687,33 @@ public function createSchedulingObject($principalUri, $objectUri, $objectData) { * @return void */ protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) { - $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions'; + $this->atomic(function () use ($calendarId, $objectUri, $operation, $calendarType) { + $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions'; - $query = $this->db->getQueryBuilder(); - $query->select('synctoken') - ->from($table) - ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); - $result = $query->executeQuery(); - $syncToken = (int)$result->fetchOne(); - $result->closeCursor(); + $query = $this->db->getQueryBuilder(); + $query->select('synctoken') + ->from($table) + ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); + $result = $query->executeQuery(); + $syncToken = (int)$result->fetchOne(); + $result->closeCursor(); - $query = $this->db->getQueryBuilder(); - $query->insert('calendarchanges') - ->values([ - 'uri' => $query->createNamedParameter($objectUri), - 'synctoken' => $query->createNamedParameter($syncToken), - 'calendarid' => $query->createNamedParameter($calendarId), - 'operation' => $query->createNamedParameter($operation), - 'calendartype' => $query->createNamedParameter($calendarType), - ]) - ->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->insert('calendarchanges') + ->values([ + 'uri' => $query->createNamedParameter($objectUri), + 'synctoken' => $query->createNamedParameter($syncToken), + 'calendarid' => $query->createNamedParameter($calendarId), + 'operation' => $query->createNamedParameter($operation), + 'calendartype' => $query->createNamedParameter($calendarType), + ]) + ->executeStatement(); - $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?"); - $stmt->execute([ - $calendarId - ]); + $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?"); + $stmt->execute([ + $calendarId + ]); + }, $this->db); } /** @@ -2805,16 +2836,18 @@ private function readBlob($cardData) { * @param list $remove */ public function updateShares(IShareable $shareable, array $add, array $remove): void { - $calendarId = $shareable->getResourceId(); - $calendarRow = $this->getCalendarById($calendarId); - if ($calendarRow === null) { - throw new \RuntimeException('Trying to update shares for innexistant calendar: ' . $calendarId); - } - $oldShares = $this->getShares($calendarId); + $this->atomic(function () use ($shareable, $add, $remove) { + $calendarId = $shareable->getResourceId(); + $calendarRow = $this->getCalendarById($calendarId); + if ($calendarRow === null) { + throw new \RuntimeException('Trying to update shares for innexistant calendar: ' . $calendarId); + } + $oldShares = $this->getShares($calendarId); - $this->calendarSharingBackend->updateShares($shareable, $add, $remove); + $this->calendarSharingBackend->updateShares($shareable, $add, $remove); - $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove)); + $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove)); + }, $this->db); } /** @@ -2830,32 +2863,34 @@ public function getShares(int $resourceId): array { * @return string|null */ public function setPublishStatus($value, $calendar) { - $calendarId = $calendar->getResourceId(); - $calendarData = $this->getCalendarById($calendarId); + return $this->atomic(function () use ($value, $calendar) { + $calendarId = $calendar->getResourceId(); + $calendarData = $this->getCalendarById($calendarId); - $query = $this->db->getQueryBuilder(); - if ($value) { - $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE); - $query->insert('dav_shares') - ->values([ - 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()), - 'type' => $query->createNamedParameter('calendar'), - 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC), - 'resourceid' => $query->createNamedParameter($calendar->getResourceId()), - 'publicuri' => $query->createNamedParameter($publicUri) - ]); - $query->executeStatement(); + $query = $this->db->getQueryBuilder(); + if ($value) { + $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE); + $query->insert('dav_shares') + ->values([ + 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()), + 'type' => $query->createNamedParameter('calendar'), + 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC), + 'resourceid' => $query->createNamedParameter($calendar->getResourceId()), + 'publicuri' => $query->createNamedParameter($publicUri) + ]); + $query->executeStatement(); - $this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri)); - return $publicUri; - } - $query->delete('dav_shares') - ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId()))) - ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))); - $query->executeStatement(); + $this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri)); + return $publicUri; + } + $query->delete('dav_shares') + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId()))) + ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))); + $query->executeStatement(); - $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData)); - return null; + $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData)); + return null; + }, $this->db); } /** @@ -2893,127 +2928,133 @@ public function applyShareAcl(int $resourceId, array $acl): array { * @param int $calendarType */ public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { - $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType); - - try { - $vCalendar = $this->readCalendarData($calendarData); - } catch (\Exception $ex) { - return; - } - - $this->purgeProperties($calendarId, $objectId); - - $query = $this->db->getQueryBuilder(); - $query->insert($this->dbObjectPropertiesTable) - ->values( - [ - 'calendarid' => $query->createNamedParameter($calendarId), - 'calendartype' => $query->createNamedParameter($calendarType), - 'objectid' => $query->createNamedParameter($objectId), - 'name' => $query->createParameter('name'), - 'parameter' => $query->createParameter('parameter'), - 'value' => $query->createParameter('value'), - ] - ); + $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType) { + $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType); - $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO']; - foreach ($vCalendar->getComponents() as $component) { - if (!in_array($component->name, $indexComponents)) { - continue; + try { + $vCalendar = $this->readCalendarData($calendarData); + } catch (\Exception $ex) { + return; } - foreach ($component->children() as $property) { - if (in_array($property->name, self::INDEXED_PROPERTIES, true)) { - $value = $property->getValue(); - // is this a shitty db? - if (!$this->db->supports4ByteText()) { - $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); - } - $value = mb_strcut($value, 0, 254); + $this->purgeProperties($calendarId, $objectId); + + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbObjectPropertiesTable) + ->values( + [ + 'calendarid' => $query->createNamedParameter($calendarId), + 'calendartype' => $query->createNamedParameter($calendarType), + 'objectid' => $query->createNamedParameter($objectId), + 'name' => $query->createParameter('name'), + 'parameter' => $query->createParameter('parameter'), + 'value' => $query->createParameter('value'), + ] + ); - $query->setParameter('name', $property->name); - $query->setParameter('parameter', null); - $query->setParameter('value', $value); - $query->executeStatement(); + $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO']; + foreach ($vCalendar->getComponents() as $component) { + if (!in_array($component->name, $indexComponents)) { + continue; } - if (array_key_exists($property->name, self::$indexParameters)) { - $parameters = $property->parameters(); - $indexedParametersForProperty = self::$indexParameters[$property->name]; + foreach ($component->children() as $property) { + if (in_array($property->name, self::INDEXED_PROPERTIES, true)) { + $value = $property->getValue(); + // is this a shitty db? + if (!$this->db->supports4ByteText()) { + $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); + } + $value = mb_strcut($value, 0, 254); - foreach ($parameters as $key => $value) { - if (in_array($key, $indexedParametersForProperty)) { - // is this a shitty db? - if ($this->db->supports4ByteText()) { - $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); - } + $query->setParameter('name', $property->name); + $query->setParameter('parameter', null); + $query->setParameter('value', $value); + $query->executeStatement(); + } - $query->setParameter('name', $property->name); - $query->setParameter('parameter', mb_strcut($key, 0, 254)); - $query->setParameter('value', mb_strcut($value, 0, 254)); - $query->executeStatement(); + if (array_key_exists($property->name, self::$indexParameters)) { + $parameters = $property->parameters(); + $indexedParametersForProperty = self::$indexParameters[$property->name]; + + foreach ($parameters as $key => $value) { + if (in_array($key, $indexedParametersForProperty)) { + // is this a shitty db? + if ($this->db->supports4ByteText()) { + $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); + } + + $query->setParameter('name', $property->name); + $query->setParameter('parameter', mb_strcut($key, 0, 254)); + $query->setParameter('value', mb_strcut($value, 0, 254)); + $query->executeStatement(); + } } } } } - } + }, $this->db); } /** * deletes all birthday calendars */ public function deleteAllBirthdayCalendars() { - $query = $this->db->getQueryBuilder(); - $result = $query->select(['id'])->from('calendars') - ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))) - ->executeQuery(); + $this->atomic(function () { + $query = $this->db->getQueryBuilder(); + $result = $query->select(['id'])->from('calendars') + ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))) + ->executeQuery(); - $ids = $result->fetchAll(); - $result->closeCursor(); - foreach ($ids as $id) { - $this->deleteCalendar( - $id['id'], - true // No data to keep in the trashbin, if the user re-enables then we regenerate - ); - } + $ids = $result->fetchAll(); + $result->closeCursor(); + foreach ($ids as $id) { + $this->deleteCalendar( + $id['id'], + true // No data to keep in the trashbin, if the user re-enables then we regenerate + ); + } + }, $this->db); } /** * @param $subscriptionId */ public function purgeAllCachedEventsForSubscription($subscriptionId) { - $query = $this->db->getQueryBuilder(); - $query->select('uri') - ->from('calendarobjects') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); - $stmt = $query->executeQuery(); + $this->atomic(function () use ($subscriptionId) { + $query = $this->db->getQueryBuilder(); + $query->select('uri') + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); + $stmt = $query->executeQuery(); - $uris = []; - foreach ($stmt->fetchAll() as $row) { - $uris[] = $row['uri']; - } - $stmt->closeCursor(); + $uris = []; + foreach ($stmt->fetchAll() as $row) { + $uris[] = $row['uri']; + } + $stmt->closeCursor(); - $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))) - ->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))) + ->executeStatement(); - $query->delete('calendarchanges') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) - ->executeStatement(); + $query->delete('calendarchanges') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->executeStatement(); - $query->delete($this->dbObjectPropertiesTable) - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) - ->executeStatement(); + $query->delete($this->dbObjectPropertiesTable) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->executeStatement(); - foreach ($uris as $uri) { - $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION); - } + foreach ($uris as $uri) { + $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION); + } + }, $this->db); } /** diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 666f1e7a85c1b..524a1ee117a72 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -50,7 +50,6 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; use OCP\IGroupManager; -use OCP\IUser; use OCP\IUserManager; use PDO; use Sabre\CardDAV\Backend\BackendInterface; @@ -61,7 +60,6 @@ use Sabre\VObject\Reader; class CardDavBackend implements BackendInterface, SyncSupport { - use TTransactional; public const PERSONAL_ADDRESSBOOK_URI = 'contacts'; @@ -145,87 +143,89 @@ public function getAddressBooksForUserCount($principalUri) { * @return array */ public function getAddressBooksForUser($principalUri) { - $principalUriOriginal = $principalUri; - $principalUri = $this->convertPrincipal($principalUri, true); - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) - ->from('addressbooks') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); - - $addressBooks = []; - - $result = $query->execute(); - while ($row = $result->fetch()) { - $addressBooks[$row['id']] = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $this->convertPrincipal($row['principaluri'], false), - '{DAV:}displayname' => $row['displayname'], - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], - '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', - ]; + return $this->atomic(function () use ($principalUri) { + $principalUriOriginal = $principalUri; + $principalUri = $this->convertPrincipal($principalUri, true); + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) + ->from('addressbooks') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); - $this->addOwnerPrincipal($addressBooks[$row['id']]); - } - $result->closeCursor(); + $addressBooks = []; - // query for shared addressbooks - $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); - $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); + $result = $query->execute(); + while ($row = $result->fetch()) { + $addressBooks[$row['id']] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $this->convertPrincipal($row['principaluri'], false), + '{DAV:}displayname' => $row['displayname'], + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', + ]; + + $this->addOwnerPrincipal($addressBooks[$row['id']]); + } + $result->closeCursor(); - $principals[] = $principalUri; + // query for shared addressbooks + $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); + $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); - $query = $this->db->getQueryBuilder(); - $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) - ->from('dav_shares', 's') - ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id')) - ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) - ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) - ->setParameter('type', 'addressbook') - ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY) - ->execute(); + $principals[] = $principalUri; - $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; - while ($row = $result->fetch()) { - if ($row['principaluri'] === $principalUri) { - continue; - } + $query = $this->db->getQueryBuilder(); + $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) + ->from('dav_shares', 's') + ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id')) + ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) + ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) + ->setParameter('type', 'addressbook') + ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY) + ->execute(); - $readOnly = (int)$row['access'] === Backend::ACCESS_READ; - if (isset($addressBooks[$row['id']])) { - if ($readOnly) { - // New share can not have more permissions then the old one. - continue; - } - if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) && - $addressBooks[$row['id']][$readOnlyPropertyName] === 0) { - // Old share is already read-write, no more permissions can be gained + $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; + while ($row = $result->fetch()) { + if ($row['principaluri'] === $principalUri) { continue; } - } - - [, $name] = \Sabre\Uri\split($row['principaluri']); - $uri = $row['uri'] . '_shared_by_' . $name; - $displayName = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? $name ?? '') . ')'; - $addressBooks[$row['id']] = [ - 'id' => $row['id'], - 'uri' => $uri, - 'principaluri' => $principalUriOriginal, - '{DAV:}displayname' => $displayName, - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], - '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], - $readOnlyPropertyName => $readOnly, - ]; + $readOnly = (int)$row['access'] === Backend::ACCESS_READ; + if (isset($addressBooks[$row['id']])) { + if ($readOnly) { + // New share can not have more permissions then the old one. + continue; + } + if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) && + $addressBooks[$row['id']][$readOnlyPropertyName] === 0) { + // Old share is already read-write, no more permissions can be gained + continue; + } + } - $this->addOwnerPrincipal($addressBooks[$row['id']]); - } - $result->closeCursor(); + [, $name] = \Sabre\Uri\split($row['principaluri']); + $uri = $row['uri'] . '_shared_by_' . $name; + $displayName = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? $name ?? '') . ')'; + + $addressBooks[$row['id']] = [ + 'id' => $row['id'], + 'uri' => $uri, + 'principaluri' => $principalUriOriginal, + '{DAV:}displayname' => $displayName, + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], + $readOnlyPropertyName => $readOnly, + ]; + + $this->addOwnerPrincipal($addressBooks[$row['id']]); + } + $result->closeCursor(); - return array_values($addressBooks); + return array_values($addressBooks); + }, $this->db); } public function getUsersOwnAddressBooks($principalUri) { @@ -333,40 +333,42 @@ public function getAddressBooksByUri(string $principal, string $addressBookUri): * @return void */ public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { - $supportedProperties = [ - '{DAV:}displayname', - '{' . Plugin::NS_CARDDAV . '}addressbook-description', - ]; + $this->atomic(function () use ($addressBookId, $propPatch) { + $supportedProperties = [ + '{DAV:}displayname', + '{' . Plugin::NS_CARDDAV . '}addressbook-description', + ]; - $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) { - $updates = []; - foreach ($mutations as $property => $newValue) { - switch ($property) { - case '{DAV:}displayname': - $updates['displayname'] = $newValue; - break; - case '{' . Plugin::NS_CARDDAV . '}addressbook-description': - $updates['description'] = $newValue; - break; + $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) { + $updates = []; + foreach ($mutations as $property => $newValue) { + switch ($property) { + case '{DAV:}displayname': + $updates['displayname'] = $newValue; + break; + case '{' . Plugin::NS_CARDDAV . '}addressbook-description': + $updates['description'] = $newValue; + break; + } } - } - $query = $this->db->getQueryBuilder(); - $query->update('addressbooks'); + $query = $this->db->getQueryBuilder(); + $query->update('addressbooks'); - foreach ($updates as $key => $value) { - $query->set($key, $query->createNamedParameter($value)); - } - $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) - ->executeStatement(); + foreach ($updates as $key => $value) { + $query->set($key, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) + ->executeStatement(); - $this->addChange($addressBookId, "", 2); + $this->addChange($addressBookId, "", 2); - $addressBookRow = $this->getAddressBookById((int)$addressBookId); - $shares = $this->getShares((int)$addressBookId); - $this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations)); + $addressBookRow = $this->getAddressBookById((int)$addressBookId); + $shares = $this->getShares((int)$addressBookId); + $this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations)); - return true; - }); + return true; + }); + }, $this->db); } /** @@ -410,7 +412,7 @@ public function createAddressBook($principalUri, $url, array $properties) { $values['displayname'] = $url; } - [$addressBookId, $addressBookRow] = $this->atomic(function() use ($values) { + [$addressBookId, $addressBookRow] = $this->atomic(function () use ($values) { $query = $this->db->getQueryBuilder(); $query->insert('addressbooks') ->values([ @@ -442,38 +444,40 @@ public function createAddressBook($principalUri, $url, array $properties) { * @return void */ public function deleteAddressBook($addressBookId) { - $addressBookId = (int)$addressBookId; - $addressBookData = $this->getAddressBookById($addressBookId); - $shares = $this->getShares($addressBookId); + $this->atomic(function () use ($addressBookId) { + $addressBookId = (int)$addressBookId; + $addressBookData = $this->getAddressBookById($addressBookId); + $shares = $this->getShares($addressBookId); - $query = $this->db->getQueryBuilder(); - $query->delete($this->dbCardsTable) - ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) - ->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT) - ->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->delete($this->dbCardsTable) + ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) + ->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT) + ->executeStatement(); - $query = $this->db->getQueryBuilder(); - $query->delete('addressbookchanges') - ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) - ->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT) - ->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->delete('addressbookchanges') + ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) + ->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT) + ->executeStatement(); - $query = $this->db->getQueryBuilder(); - $query->delete('addressbooks') - ->where($query->expr()->eq('id', $query->createParameter('id'))) - ->setParameter('id', $addressBookId, IQueryBuilder::PARAM_INT) - ->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->delete('addressbooks') + ->where($query->expr()->eq('id', $query->createParameter('id'))) + ->setParameter('id', $addressBookId, IQueryBuilder::PARAM_INT) + ->executeStatement(); - $this->sharingBackend->deleteAllShares($addressBookId); + $this->sharingBackend->deleteAllShares($addressBookId); - $query = $this->db->getQueryBuilder(); - $query->delete($this->dbCardsPropertiesTable) - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId, IQueryBuilder::PARAM_INT))) - ->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->delete($this->dbCardsPropertiesTable) + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); - if ($addressBookData) { - $this->dispatcher->dispatchTyped(new AddressBookDeletedEvent($addressBookId, $addressBookData, $shares)); - } + if ($addressBookData) { + $this->dispatcher->dispatchTyped(new AddressBookDeletedEvent($addressBookId, $addressBookData, $shares)); + } + }, $this->db); } /** @@ -631,47 +635,48 @@ public function getMultipleCards($addressBookId, array $uris) { public function createCard($addressBookId, $cardUri, $cardData, bool $checkAlreadyExists = true) { $etag = md5($cardData); $uid = $this->getUID($cardData); - - if ($checkAlreadyExists) { - $q = $this->db->getQueryBuilder(); - $q->select('uid') - ->from($this->dbCardsTable) - ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId))) - ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid))) - ->setMaxResults(1); - $result = $q->executeQuery(); - $count = (bool)$result->fetchOne(); - $result->closeCursor(); - if ($count) { - throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.'); + return $this->atomic(function () use ($addressBookId, $cardUri, $cardData, $checkAlreadyExists, $etag, $uid) { + if ($checkAlreadyExists) { + $q = $this->db->getQueryBuilder(); + $q->select('uid') + ->from($this->dbCardsTable) + ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId))) + ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid))) + ->setMaxResults(1); + $result = $q->executeQuery(); + $count = (bool)$result->fetchOne(); + $result->closeCursor(); + if ($count) { + throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.'); + } } - } - $query = $this->db->getQueryBuilder(); - $query->insert('cards') - ->values([ - 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB), - 'uri' => $query->createNamedParameter($cardUri), - 'lastmodified' => $query->createNamedParameter(time()), - 'addressbookid' => $query->createNamedParameter($addressBookId), - 'size' => $query->createNamedParameter(strlen($cardData)), - 'etag' => $query->createNamedParameter($etag), - 'uid' => $query->createNamedParameter($uid), - ]) - ->execute(); + $query = $this->db->getQueryBuilder(); + $query->insert('cards') + ->values([ + 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB), + 'uri' => $query->createNamedParameter($cardUri), + 'lastmodified' => $query->createNamedParameter(time()), + 'addressbookid' => $query->createNamedParameter($addressBookId), + 'size' => $query->createNamedParameter(strlen($cardData)), + 'etag' => $query->createNamedParameter($etag), + 'uid' => $query->createNamedParameter($uid), + ]) + ->execute(); - $etagCacheKey = "$addressBookId#$cardUri"; - $this->etagCache[$etagCacheKey] = $etag; + $etagCacheKey = "$addressBookId#$cardUri"; + $this->etagCache[$etagCacheKey] = $etag; - $this->addChange($addressBookId, $cardUri, 1); - $this->updateProperties($addressBookId, $cardUri, $cardData); + $this->addChange($addressBookId, $cardUri, 1); + $this->updateProperties($addressBookId, $cardUri, $cardData); - $addressBookData = $this->getAddressBookById($addressBookId); - $shares = $this->getShares($addressBookId); - $objectRow = $this->getCard($addressBookId, $cardUri); - $this->dispatcher->dispatchTyped(new CardCreatedEvent($addressBookId, $addressBookData, $shares, $objectRow)); + $addressBookData = $this->getAddressBookById($addressBookId); + $shares = $this->getShares($addressBookId); + $objectRow = $this->getCard($addressBookId, $cardUri); + $this->dispatcher->dispatchTyped(new CardCreatedEvent($addressBookId, $addressBookData, $shares, $objectRow)); - return '"' . $etag . '"'; + return '"' . $etag . '"'; + }, $this->db); } /** @@ -702,34 +707,37 @@ public function createCard($addressBookId, $cardUri, $cardData, bool $checkAlrea public function updateCard($addressBookId, $cardUri, $cardData) { $uid = $this->getUID($cardData); $etag = md5($cardData); - $query = $this->db->getQueryBuilder(); - // check for recently stored etag and stop if it is the same - $etagCacheKey = "$addressBookId#$cardUri"; - if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) { - return '"' . $etag . '"'; - } + return $this->atomic(function () use ($addressBookId, $cardUri, $cardData, $uid, $etag) { + $query = $this->db->getQueryBuilder(); - $query->update($this->dbCardsTable) - ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB)) - ->set('lastmodified', $query->createNamedParameter(time())) - ->set('size', $query->createNamedParameter(strlen($cardData))) - ->set('etag', $query->createNamedParameter($etag)) - ->set('uid', $query->createNamedParameter($uid)) - ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->execute(); + // check for recently stored etag and stop if it is the same + $etagCacheKey = "$addressBookId#$cardUri"; + if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) { + return '"' . $etag . '"'; + } + + $query->update($this->dbCardsTable) + ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB)) + ->set('lastmodified', $query->createNamedParameter(time())) + ->set('size', $query->createNamedParameter(strlen($cardData))) + ->set('etag', $query->createNamedParameter($etag)) + ->set('uid', $query->createNamedParameter($uid)) + ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->execute(); - $this->etagCache[$etagCacheKey] = $etag; + $this->etagCache[$etagCacheKey] = $etag; - $this->addChange($addressBookId, $cardUri, 2); - $this->updateProperties($addressBookId, $cardUri, $cardData); + $this->addChange($addressBookId, $cardUri, 2); + $this->updateProperties($addressBookId, $cardUri, $cardData); - $addressBookData = $this->getAddressBookById($addressBookId); - $shares = $this->getShares($addressBookId); - $objectRow = $this->getCard($addressBookId, $cardUri); - $this->dispatcher->dispatchTyped(new CardUpdatedEvent($addressBookId, $addressBookData, $shares, $objectRow)); - return '"' . $etag . '"'; + $addressBookData = $this->getAddressBookById($addressBookId); + $shares = $this->getShares($addressBookId); + $objectRow = $this->getCard($addressBookId, $cardUri); + $this->dispatcher->dispatchTyped(new CardUpdatedEvent($addressBookId, $addressBookData, $shares, $objectRow)); + return '"' . $etag . '"'; + }, $this->db); } /** @@ -740,32 +748,34 @@ public function updateCard($addressBookId, $cardUri, $cardData) { * @return bool */ public function deleteCard($addressBookId, $cardUri) { - $addressBookData = $this->getAddressBookById($addressBookId); - $shares = $this->getShares($addressBookId); - $objectRow = $this->getCard($addressBookId, $cardUri); - - try { - $cardId = $this->getCardId($addressBookId, $cardUri); - } catch (\InvalidArgumentException $e) { - $cardId = null; - } - $query = $this->db->getQueryBuilder(); - $ret = $query->delete($this->dbCardsTable) - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) - ->executeStatement(); + return $this->atomic(function () use ($addressBookId, $cardUri) { + $addressBookData = $this->getAddressBookById($addressBookId); + $shares = $this->getShares($addressBookId); + $objectRow = $this->getCard($addressBookId, $cardUri); + + try { + $cardId = $this->getCardId($addressBookId, $cardUri); + } catch (\InvalidArgumentException $e) { + $cardId = null; + } + $query = $this->db->getQueryBuilder(); + $ret = $query->delete($this->dbCardsTable) + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) + ->executeStatement(); - $this->addChange($addressBookId, $cardUri, 3); + $this->addChange($addressBookId, $cardUri, 3); - if ($ret === 1) { - if ($cardId !== null) { - $this->dispatcher->dispatchTyped(new CardDeletedEvent($addressBookId, $addressBookData, $shares, $objectRow)); - $this->purgeProperties($addressBookId, $cardId); + if ($ret === 1) { + if ($cardId !== null) { + $this->dispatcher->dispatchTyped(new CardDeletedEvent($addressBookId, $addressBookData, $shares, $objectRow)); + $this->purgeProperties($addressBookId, $cardId); + } + return true; } - return true; - } - return false; + return false; + }, $this->db); } /** @@ -826,81 +836,83 @@ public function deleteCard($addressBookId, $cardUri) { */ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { // Current synctoken - $qb = $this->db->getQueryBuilder(); - $qb->select('synctoken') - ->from('addressbooks') - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($addressBookId)) - ); - $stmt = $qb->executeQuery(); - $currentToken = $stmt->fetchOne(); - $stmt->closeCursor(); - - if (is_null($currentToken)) { - return []; - } - - $result = [ - 'syncToken' => $currentToken, - 'added' => [], - 'modified' => [], - 'deleted' => [], - ]; - - if ($syncToken) { + return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) { $qb = $this->db->getQueryBuilder(); - $qb->select('uri', 'operation') - ->from('addressbookchanges') + $qb->select('synctoken') + ->from('addressbooks') ->where( - $qb->expr()->andX( - $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)), - $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)), - $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)) - ) - )->orderBy('synctoken'); + $qb->expr()->eq('id', $qb->createNamedParameter($addressBookId)) + ); + $stmt = $qb->executeQuery(); + $currentToken = $stmt->fetchOne(); + $stmt->closeCursor(); - if (is_int($limit) && $limit > 0) { - $qb->setMaxResults($limit); + if (is_null($currentToken)) { + return []; } - // Fetching all changes - $stmt = $qb->executeQuery(); + $result = [ + 'syncToken' => $currentToken, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; - $changes = []; + if ($syncToken) { + $qb = $this->db->getQueryBuilder(); + $qb->select('uri', 'operation') + ->from('addressbookchanges') + ->where( + $qb->expr()->andX( + $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)), + $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)), + $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)) + ) + )->orderBy('synctoken'); + + if (is_int($limit) && $limit > 0) { + $qb->setMaxResults($limit); + } - // This loop ensures that any duplicates are overwritten, only the - // last change on a node is relevant. - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - $changes[$row['uri']] = $row['operation']; - } - $stmt->closeCursor(); + // Fetching all changes + $stmt = $qb->executeQuery(); - 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; + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $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 { + $qb = $this->db->getQueryBuilder(); + $qb->select('uri') + ->from('cards') + ->where( + $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)) + ); + // No synctoken supplied, this is the initial sync. + $stmt = $qb->executeQuery(); + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + $stmt->closeCursor(); } - } else { - $qb = $this->db->getQueryBuilder(); - $qb->select('uri') - ->from('cards') - ->where( - $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)) - ); - // No synctoken supplied, this is the initial sync. - $stmt = $qb->executeQuery(); - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); - $stmt->closeCursor(); - } - return $result; + return $result; + }, $this->db); } /** @@ -912,18 +924,20 @@ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, * @return void */ protected function addChange($addressBookId, $objectUri, $operation) { - $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?'; - $stmt = $this->db->prepare($sql); - $stmt->execute([ - $objectUri, - $addressBookId, - $operation, - $addressBookId - ]); - $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); - $stmt->execute([ - $addressBookId - ]); + $this->atomic(function () use ($addressBookId, $objectUri, $operation) { + $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?'; + $stmt = $this->db->prepare($sql); + $stmt->execute([ + $objectUri, + $addressBookId, + $operation, + $addressBookId + ]); + $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); + $stmt->execute([ + $addressBookId + ]); + }, $this->db); } /** @@ -973,13 +987,15 @@ private function readBlob($cardData, &$modified = false) { * @param list $remove */ public function updateShares(IShareable $shareable, array $add, array $remove): void { - $addressBookId = $shareable->getResourceId(); - $addressBookData = $this->getAddressBookById($addressBookId); - $oldShares = $this->getShares($addressBookId); + $this->atomic(function () use ($shareable, $add, $remove) { + $addressBookId = $shareable->getResourceId(); + $addressBookData = $this->getAddressBookById($addressBookId); + $oldShares = $this->getShares($addressBookId); - $this->sharingBackend->updateShares($shareable, $add, $remove); + $this->sharingBackend->updateShares($shareable, $add, $remove); - $this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove)); + $this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove)); + }, $this->db); } /** @@ -998,7 +1014,9 @@ public function updateShares(IShareable $shareable, array $add, array $remove): * @return array an array of contacts which are arrays of key-value-pairs */ public function search($addressBookId, $pattern, $searchProperties, $options = []): array { - return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options); + return $this->atomic(function () use ($addressBookId, $pattern, $searchProperties, $options) { + return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options); + }, $this->db); } /** @@ -1014,11 +1032,13 @@ public function searchPrincipalUri(string $principalUri, string $pattern, array $searchProperties, array $options = []): array { - $addressBookIds = array_map(static function ($row):int { - return (int) $row['id']; - }, $this->getAddressBooksForUser($principalUri)); + return $this->atomic(function () use ($principalUri, $pattern, $searchProperties, $options) { + $addressBookIds = array_map(static function ($row):int { + return (int) $row['id']; + }, $this->getAddressBooksForUser($principalUri)); - return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options); + return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options); + }, $this->db); } /** @@ -1219,27 +1239,24 @@ public function getShares(int $addressBookId): array { * @param string $vCardSerialized */ protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) { - $cardId = $this->getCardId($addressBookId, $cardUri); - $vCard = $this->readCard($vCardSerialized); - - $this->purgeProperties($addressBookId, $cardId); - - $query = $this->db->getQueryBuilder(); - $query->insert($this->dbCardsPropertiesTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter($addressBookId), - 'cardid' => $query->createNamedParameter($cardId), - 'name' => $query->createParameter('name'), - 'value' => $query->createParameter('value'), - 'preferred' => $query->createParameter('preferred') - ] - ); + $this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized) { + $cardId = $this->getCardId($addressBookId, $cardUri); + $vCard = $this->readCard($vCardSerialized); + $this->purgeProperties($addressBookId, $cardId); - $this->db->beginTransaction(); + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter($addressBookId), + 'cardid' => $query->createNamedParameter($cardId), + 'name' => $query->createParameter('name'), + 'value' => $query->createParameter('value'), + 'preferred' => $query->createParameter('preferred') + ] + ); - try { foreach ($vCard->children() as $property) { if (!in_array($property->name, self::$indexProperties)) { continue; @@ -1256,10 +1273,7 @@ protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) $query->setParameter('preferred', $preferred); $query->execute(); } - $this->db->commit(); - } catch (\Exception $e) { - $this->db->rollBack(); - } + }, $this->db); } /** diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php index 7ba5ffd270004..813f99dcbbdbe 100644 --- a/apps/dav/lib/DAV/Sharing/Backend.php +++ b/apps/dav/lib/DAV/Sharing/Backend.php @@ -29,12 +29,15 @@ namespace OCA\DAV\DAV\Sharing; use OCA\DAV\Connector\Sabre\Principal; +use OCP\AppFramework\Db\TTransactional; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUserManager; use OCP\DB\QueryBuilder\IQueryBuilder; class Backend { + use TTransactional; + private IDBConnection $db; private IUserManager $userManager; private IGroupManager $groupManager; @@ -58,18 +61,20 @@ public function __construct(IDBConnection $db, IUserManager $userManager, IGroup * @param list $remove */ public function updateShares(IShareable $shareable, array $add, array $remove): void { - foreach ($add as $element) { - $principal = $this->principalBackend->findByUri($element['href'], ''); - if ($principal !== '') { - $this->shareWith($shareable, $element); + $this->atomic(function () use ($shareable, $add, $remove) { + foreach ($add as $element) { + $principal = $this->principalBackend->findByUri($element['href'], ''); + if ($principal !== '') { + $this->shareWith($shareable, $element); + } } - } - foreach ($remove as $element) { - $principal = $this->principalBackend->findByUri($element, ''); - if ($principal !== '') { - $this->unshare($shareable, $element); + foreach ($remove as $element) { + $principal = $this->principalBackend->findByUri($element, ''); + if ($principal !== '') { + $this->unshare($shareable, $element); + } } - } + }, $this->db); } /** diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 7b093cc1a4ebd..ac3c786f5a689 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -130,7 +130,6 @@ null - null