diff options
21 files changed, 1507 insertions, 1399 deletions
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index d11aaa06a1a..70f2ca8290f 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -315,132 +315,134 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ 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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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); } /** @@ -2657,32 +2686,35 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param int $calendarType * @return void */ - protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) { + protected function addChange(int $calendarId, string $objectUri, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void { $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(); + $this->atomic(function () use ($calendarId, $objectUri, $operation, $calendarType, $table) { + $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 - ]); + $query = $this->db->getQueryBuilder(); + $query->update($table) + ->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))) + ->executeStatement(); + }, $this->db); } /** @@ -2805,16 +2837,18 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param list<string> $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 +2864,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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 +2929,133 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @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/CalDAV/Reminder/INotificationProvider.php b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php index 505960ed662..e8cbfdd0ab3 100644 --- a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php @@ -42,13 +42,13 @@ interface INotificationProvider { * Send notification * * @param VEvent $vevent - * @param string $calendarDisplayName + * @param string|null $calendarDisplayName * @param string[] $principalEmailAddresses All email addresses associated to the principal owning the calendar object * @param IUser[] $users * @return void */ public function send(VEvent $vevent, - string $calendarDisplayName, + ?string $calendarDisplayName, array $principalEmailAddresses, array $users = []): void; } diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php index 6986328facd..bccbec5fe3c 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php @@ -82,13 +82,13 @@ abstract class AbstractProvider implements INotificationProvider { * Send notification * * @param VEvent $vevent - * @param string $calendarDisplayName + * @param string|null $calendarDisplayName * @param string[] $principalEmailAddresses * @param IUser[] $users * @return void */ abstract public function send(VEvent $vevent, - string $calendarDisplayName, + ?string $calendarDisplayName, array $principalEmailAddresses, array $users = []): void; @@ -185,4 +185,8 @@ abstract class AbstractProvider implements INotificationProvider { return clone $vevent->DTSTART; } + + protected function getCalendarDisplayNameFallback(string $lang): string { + return $this->getL10NForLang($lang)->t('Untitled calendar'); + } } diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php index 32072688967..da275efdcf1 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php @@ -70,13 +70,13 @@ class EmailProvider extends AbstractProvider { * Send out notification via email * * @param VEvent $vevent - * @param string $calendarDisplayName + * @param string|null $calendarDisplayName * @param string[] $principalEmailAddresses * @param array $users * @throws \Exception */ public function send(VEvent $vevent, - string $calendarDisplayName, + ?string $calendarDisplayName, array $principalEmailAddresses, array $users = []):void { $fallbackLanguage = $this->getFallbackLanguage(); @@ -115,7 +115,7 @@ class EmailProvider extends AbstractProvider { $template = $this->mailer->createEMailTemplate('dav.calendarReminder'); $template->addHeader(); $this->addSubjectAndHeading($template, $l10n, $vevent); - $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent); + $this->addBulletList($template, $l10n, $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($lang), $vevent); $template->addFooter(); foreach ($emailAddresses as $emailAddress) { diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php index 833d74079aa..be8bafd2f35 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php @@ -73,13 +73,13 @@ class PushProvider extends AbstractProvider { * Send push notification to all users. * * @param VEvent $vevent - * @param string $calendarDisplayName + * @param string|null $calendarDisplayName * @param string[] $principalEmailAddresses * @param IUser[] $users * @throws \Exception */ public function send(VEvent $vevent, - string $calendarDisplayName, + ?string $calendarDisplayName, array $principalEmailAddresses, array $users = []):void { if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'no') !== 'yes') { @@ -87,7 +87,6 @@ class PushProvider extends AbstractProvider { } $eventDetails = $this->extractEventDetails($vevent); - $eventDetails['calendar_displayname'] = $calendarDisplayName; $eventUUID = (string) $vevent->UID; if (!$eventUUID) { return; @@ -95,6 +94,8 @@ class PushProvider extends AbstractProvider { $eventUUIDHash = hash('sha256', $eventUUID, false); foreach ($users as $user) { + $eventDetails['calendar_displayname'] = $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($this->l10nFactory->getUserLanguage($user)); + /** @var INotification $notification */ $notification = $this->manager->createNotification(); $notification->setApp(Application::APP_ID) diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php index a2daa3cc98e..bca154a48e8 100644 --- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php +++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php @@ -147,7 +147,14 @@ class ReminderService { continue; } - $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']); + try { + $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']); + } catch (MaxInstancesExceededException $e) { + $this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]); + $this->backend->removeReminder($reminder['id']); + continue; + } + if (!$vevent) { $this->logger->debug('Reminder {id} does not belong to a valid event', [ 'id' => $reminder['id'], diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index 76e84a2b54b..329197445dd 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -217,10 +217,12 @@ class IMipPlugin extends SabreIMipPlugin { $sender = substr($iTipMessage->sender, 7); + $replyingAttendee = null; switch (strtolower($iTipMessage->method)) { case self::METHOD_REPLY: $method = self::METHOD_REPLY; $data = $this->imipService->buildBodyData($vEvent, $oldVevent); + $replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage); break; case self::METHOD_CANCEL: $method = self::METHOD_CANCEL; @@ -256,7 +258,7 @@ class IMipPlugin extends SabreIMipPlugin { $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data); $template->addHeader(); - $this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title'], $isModified); + $this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title'], $isModified, $replyingAttendee); $this->imipService->addBulletList($template, $vEvent, $data); // Only add response buttons to invitation requests: Fix Issue #11230 diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php index 034a59a98d4..8596500f320 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipService.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php @@ -366,7 +366,7 @@ class IMipService { * @param bool $isModified */ public function addSubjectAndHeading(IEMailTemplate $template, - string $method, string $sender, string $summary, bool $isModified): void { + string $method, string $sender, string $summary, bool $isModified, ?Property $replyingAttendee = null): void { if ($method === IMipPlugin::METHOD_CANCEL) { // TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}" $template->setSubject($this->l10n->t('Cancelled: %1$s', [$summary])); @@ -374,7 +374,24 @@ class IMipService { } elseif ($method === IMipPlugin::METHOD_REPLY) { // TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}" $template->setSubject($this->l10n->t('Re: %1$s', [$summary])); - $template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender])); + // Build the strings + $partstat = (isset($replyingAttendee)) ? $replyingAttendee->offsetGet('PARTSTAT') : null; + $partstat = ($partstat instanceof Parameter) ? $partstat->getValue() : null; + switch ($partstat) { + case 'ACCEPTED': + $template->addHeading($this->l10n->t('%1$s has accepted your invitation', [$sender])); + break; + case 'TENTATIVE': + $template->addHeading($this->l10n->t('%1$s has tentatively accepted your invitation', [$sender])); + break; + case 'DECLINED': + $template->addHeading($this->l10n->t('%1$s has declined your invitation', [$sender])); + break; + case null: + default: + $template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender])); + break; + } } elseif ($method === IMipPlugin::METHOD_REQUEST && $isModified) { // TRANSLATORS Subject for email, when an invitation is updated. Ex: "Invitation updated: {{Event Name}}" $template->setSubject($this->l10n->t('Invitation updated: %1$s', [$summary])); @@ -603,4 +620,17 @@ class IMipService { $template->addBodyText($html, $text); } + + public function getReplyingAttendee(Message $iTipMessage): ?Property { + /** @var VEvent $vevent */ + $vevent = $iTipMessage->message->VEVENT; + $attendees = $vevent->select('ATTENDEE'); + foreach ($attendees as $attendee) { + /** @var Property $attendee */ + if (strcasecmp($attendee->getValue(), $iTipMessage->sender) === 0) { + return $attendee; + } + } + return null; + } } diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 666f1e7a85c..577d7282eae 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -50,7 +50,6 @@ use OCP\DB\QueryBuilder\IQueryBuilder; 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\Component\VCard; use Sabre\VObject\Reader; class CardDavBackend implements BackendInterface, SyncSupport { - use TTransactional; public const PERSONAL_ADDRESSBOOK_URI = 'contacts'; @@ -145,87 +143,89 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @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))); + 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))); - $addressBooks = []; + $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', - ]; - - $this->addOwnerPrincipal($addressBooks[$row['id']]); - } - $result->closeCursor(); + $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(); - // query for shared addressbooks - $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); - $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); + // query for shared addressbooks + $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); + $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); - $principals[] = $principalUri; + $principals[] = $principalUri; - $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(); - - $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 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @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 @@ class CardDavBackend implements BackendInterface, SyncSupport { $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 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @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 @@ class CardDavBackend implements BackendInterface, SyncSupport { 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 @@ class CardDavBackend implements BackendInterface, SyncSupport { 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 . '"'; + } - $this->etagCache[$etagCacheKey] = $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->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 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @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 @@ class CardDavBackend implements BackendInterface, SyncSupport { */ 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); } /** @@ -911,19 +923,32 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param int $operation 1 = add, 2 = modify, 3 = delete * @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 - ]); + protected function addChange(int $addressBookId, string $objectUri, int $operation): void { + $this->atomic(function () use ($addressBookId, $objectUri, $operation) { + $query = $this->db->getQueryBuilder(); + $query->select('synctoken') + ->from('addressbooks') + ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))); + $result = $query->executeQuery(); + $syncToken = (int)$result->fetchOne(); + $result->closeCursor(); + + $query = $this->db->getQueryBuilder(); + $query->insert('addressbookchanges') + ->values([ + 'uri' => $query->createNamedParameter($objectUri), + 'synctoken' => $query->createNamedParameter($syncToken), + 'addressbookid' => $query->createNamedParameter($addressBookId), + 'operation' => $query->createNamedParameter($operation), + ]) + ->executeStatement(); + + $query = $this->db->getQueryBuilder(); + $query->update('addressbooks') + ->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) + ->executeStatement(); + }, $this->db); } /** @@ -973,13 +998,15 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param list<string> $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 +1025,9 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @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 +1043,13 @@ class CardDavBackend implements BackendInterface, SyncSupport { 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 +1250,24 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @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 +1284,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $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 7ba5ffd2700..813f99dcbbd 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 @@ class Backend { * @param list<string> $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/apps/encryption/lib/Crypto/Crypt.php b/apps/encryption/lib/Crypto/Crypt.php index 22a697a1232..0cf6451d287 100644 --- a/apps/encryption/lib/Crypto/Crypt.php +++ b/apps/encryption/lib/Crypto/Crypt.php @@ -523,10 +523,12 @@ class Crypt { $signature = $this->createSignature($data, $passPhrase); $isCorrectHash = hash_equals($expectedSignature, $signature); - if (!$isCorrectHash && $enforceSignature) { - throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature')); - } elseif (!$isCorrectHash && !$enforceSignature) { - $this->logger->info("Signature check skipped", ['app' => 'encryption']); + if (!$isCorrectHash) { + if ($enforceSignature) { + throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature')); + } else { + $this->logger->info("Signature check skipped", ['app' => 'encryption']); + } } } diff --git a/apps/files_versions/lib/Capabilities.php b/apps/files_versions/lib/Capabilities.php index 6524943690a..e97a5c4aaa4 100644 --- a/apps/files_versions/lib/Capabilities.php +++ b/apps/files_versions/lib/Capabilities.php @@ -46,7 +46,7 @@ class Capabilities implements ICapability { * @return array */ public function getCapabilities() { - $groupFolderOrS3VersioningInstalled = $this->appManager->isInstalled('groupfolders') || $this->appManager->isInstalled('groupfolders'); + $groupFolderOrS3VersioningInstalled = $this->appManager->isInstalled('groupfolders') || $this->appManager->isInstalled('files_versions_s3'); return [ 'files' => [ diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 7b093cc1a4e..ac3c786f5a6 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -130,7 +130,6 @@ </MoreSpecificImplementedParamType> <NullableReturnStatement occurrences="2"> <code>null</code> - <code>null</code> </NullableReturnStatement> </file> <file src="apps/dav/lib/CalDAV/CalendarHome.php"> diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 824e2e056c8..b1fa509d8c0 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -327,18 +327,20 @@ class PublicKeyTokenProvider implements IProvider { throw new InvalidTokenException("Invalid token type"); } - // When changing passwords all temp tokens are deleted - $this->mapper->deleteTempToken($token); - - // Update the password for all tokens - $tokens = $this->mapper->getTokenByUser($token->getUID()); - $hashedPassword = $this->hashPassword($password); - foreach ($tokens as $t) { - $publicKey = $t->getPublicKey(); - $t->setPassword($this->encryptPassword($password, $publicKey)); - $t->setPasswordHash($hashedPassword); - $this->updateToken($t); - } + $this->atomic(function () use ($password, $token) { + // When changing passwords all temp tokens are deleted + $this->mapper->deleteTempToken($token); + + // Update the password for all tokens + $tokens = $this->mapper->getTokenByUser($token->getUID()); + $hashedPassword = $this->hashPassword($password); + foreach ($tokens as $t) { + $publicKey = $t->getPublicKey(); + $t->setPassword($this->encryptPassword($password, $publicKey)); + $t->setPasswordHash($hashedPassword); + $this->updateToken($t); + } + }, $this->db); } private function hashPassword(string $password): string { @@ -489,49 +491,51 @@ class PublicKeyTokenProvider implements IProvider { return; } - // Update the password for all tokens - $tokens = $this->mapper->getTokenByUser($uid); - $newPasswordHash = null; - - /** - * - true: The password hash could not be verified anymore - * and the token needs to be updated with the newly encrypted password - * - false: The hash could still be verified - * - missing: The hash needs to be verified - */ - $hashNeedsUpdate = []; - - foreach ($tokens as $t) { - if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) { - if ($t->getPasswordHash() === null) { - $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true; - } elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) { - $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true; - } else { - $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false; + $this->atomic(function () use ($password, $uid) { + // Update the password for all tokens + $tokens = $this->mapper->getTokenByUser($uid); + $newPasswordHash = null; + + /** + * - true: The password hash could not be verified anymore + * and the token needs to be updated with the newly encrypted password + * - false: The hash could still be verified + * - missing: The hash needs to be verified + */ + $hashNeedsUpdate = []; + + foreach ($tokens as $t) { + if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) { + if ($t->getPasswordHash() === null) { + $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true; + } elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) { + $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true; + } else { + $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false; + } } - } - $needsUpdating = $hashNeedsUpdate[$t->getPasswordHash() ?: ''] ?? true; - - if ($needsUpdating) { - if ($newPasswordHash === null) { - $newPasswordHash = $this->hashPassword($password); + $needsUpdating = $hashNeedsUpdate[$t->getPasswordHash() ?: ''] ?? true; + + if ($needsUpdating) { + if ($newPasswordHash === null) { + $newPasswordHash = $this->hashPassword($password); + } + + $publicKey = $t->getPublicKey(); + $t->setPassword($this->encryptPassword($password, $publicKey)); + $t->setPasswordHash($newPasswordHash); + $t->setPasswordInvalid(false); + $this->updateToken($t); } - - $publicKey = $t->getPublicKey(); - $t->setPassword($this->encryptPassword($password, $publicKey)); - $t->setPasswordHash($newPasswordHash); - $t->setPasswordInvalid(false); - $this->updateToken($t); } - } - // If password hashes are different we update them all to be equal so - // that the next execution only needs to verify once - if (count($hashNeedsUpdate) > 1) { - $newPasswordHash = $this->hashPassword($password); - $this->mapper->updateHashesForUser($uid, $newPasswordHash); - } + // If password hashes are different we update them all to be equal so + // that the next execution only needs to verify once + if (count($hashNeedsUpdate) > 1) { + $newPasswordHash = $this->hashPassword($password); + $this->mapper->updateHashesForUser($uid, $newPasswordHash); + } + }, $this->db); } private function logOpensslError() { diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index 8ca5cf53d16..921c50fd958 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -98,6 +98,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil public function mkdir($path) { $path = $this->normalizePath($path); if ($this->file_exists($path)) { + $this->logger->warning("Tried to create an object store folder that already exists: $path"); return false; } @@ -121,10 +122,12 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil if ($parentType === false) { if (!$this->mkdir($parent)) { // something went wrong + $this->logger->warning("Parent folder ($parent) doesn't exist and couldn't be created"); return false; } } elseif ($parentType === 'file') { // parent is a file + $this->logger->warning("Parent ($parent) is a file"); return false; } // finally create the new dir diff --git a/lib/private/Files/ObjectStore/S3.php b/lib/private/Files/ObjectStore/S3.php index ebc8886f12d..76eee2bc962 100644 --- a/lib/private/Files/ObjectStore/S3.php +++ b/lib/private/Files/ObjectStore/S3.php @@ -69,13 +69,24 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload { } public function getMultipartUploads(string $urn, string $uploadId): array { - $parts = $this->getConnection()->listParts([ - 'Bucket' => $this->bucket, - 'Key' => $urn, - 'UploadId' => $uploadId, - 'MaxParts' => 10000 - ]); - return $parts->get('Parts') ?? []; + $parts = []; + $isTruncated = true; + $partNumberMarker = 0; + + while ($isTruncated) { + $result = $this->getConnection()->listParts([ + 'Bucket' => $this->bucket, + 'Key' => $urn, + 'UploadId' => $uploadId, + 'MaxParts' => 1000, + 'PartNumberMarker' => $partNumberMarker + ]); + $parts = array_merge($parts, $result->get('Parts') ?? []); + $isTruncated = $result->get('IsTruncated'); + $partNumberMarker = $result->get('NextPartNumberMarker'); + } + + return $parts; } public function completeMultipartUpload(string $urn, string $uploadId, array $result): int { diff --git a/lib/private/Files/Type/Loader.php b/lib/private/Files/Type/Loader.php index bf5af36ec6e..32013bc3786 100644 --- a/lib/private/Files/Type/Loader.php +++ b/lib/private/Files/Type/Loader.php @@ -24,6 +24,9 @@ */ namespace OC\Files\Type; +use OC\DB\Exceptions\DbalException; +use OCP\AppFramework\Db\TTransactional; +use OCP\DB\Exception as DBException; use OCP\Files\IMimeTypeLoader; use OCP\IDBConnection; @@ -33,6 +36,8 @@ use OCP\IDBConnection; * @package OC\Files\Type */ class Loader implements IMimeTypeLoader { + use TTransactional; + /** @var IDBConnection */ private $dbConnection; @@ -108,31 +113,49 @@ class Loader implements IMimeTypeLoader { * Store a mimetype in the DB * * @param string $mimetype - * @param int inserted ID + * @return int inserted ID */ protected function store($mimetype) { - $this->dbConnection->insertIfNotExist('*PREFIX*mimetypes', [ - 'mimetype' => $mimetype - ]); - - $fetch = $this->dbConnection->getQueryBuilder(); - $fetch->select('id') - ->from('mimetypes') - ->where( - $fetch->expr()->eq('mimetype', $fetch->createNamedParameter($mimetype) - )); - - $result = $fetch->execute(); - $row = $result->fetch(); - $result->closeCursor(); + $row = $this->atomic(function () use ($mimetype) { + try { + $insert = $this->dbConnection->getQueryBuilder(); + $insert->insert('mimetypes') + ->values([ + 'mimetype' => $insert->createNamedParameter($mimetype) + ]) + ->executeStatement(); + return [ + 'mimetype' => $mimetype, + 'id' => $insert->getLastInsertId(), + ]; + } catch (DbalException $e) { + if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } + $qb = $this->dbConnection->getQueryBuilder(); + $row = $qb->select('id') + ->from('mimetypes') + ->where($qb->expr()->eq('mimetype', $qb->createNamedParameter($mimetype))) + ->executeQuery() + ->fetchOne(); + if ($row) { + return [ + 'mimetype' => $mimetype, + 'id' => $row['id'], + ]; + } + throw new \Exception("Database threw an unique constraint on inserting a new mimetype, but couldn't return the ID for this very mimetype"); + } + }, $this->dbConnection); if (!$row) { throw new \Exception("Failed to get mimetype id for $mimetype after trying to store it"); } + $mimetypeId = (int) $row['id']; - $this->mimetypes[$row['id']] = $mimetype; - $this->mimetypeIds[$mimetype] = $row['id']; - return $row['id']; + $this->mimetypes[$mimetypeId] = $mimetype; + $this->mimetypeIds[$mimetype] = $mimetypeId; + return $mimetypeId; } /** diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 47f2952c6e6..ed9474fafb2 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -137,6 +137,8 @@ class Generator { } $previewFolder = $this->getPreviewFolder($file); + // List every existing preview first instead of trying to find them one by one + $previewFiles = $previewFolder->getDirectoryListing(); $previewVersion = ''; if ($file instanceof IVersionedPreviewFile) { @@ -150,7 +152,7 @@ class Generator { && preg_match(Imaginary::supportedMimeTypes(), $mimeType) && $this->config->getSystemValueString('preview_imaginary_url', 'invalid') !== 'invalid') { $crop = $specifications[0]['crop'] ?? false; - $preview = $this->getSmallImagePreview($previewFolder, $file, $mimeType, $previewVersion, $crop); + $preview = $this->getSmallImagePreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion, $crop); if ($preview->getSize() === 0) { $preview->delete(); @@ -161,7 +163,7 @@ class Generator { } // Get the max preview and infer the max preview sizes from that - $maxPreview = $this->getMaxPreview($previewFolder, $file, $mimeType, $previewVersion); + $maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion); $maxPreviewImage = null; // only load the image when we need it if ($maxPreview->getSize() === 0) { $maxPreview->delete(); @@ -197,7 +199,7 @@ class Generator { // Try to get a cached preview. Else generate (and store) one try { try { - $preview = $this->getCachedPreview($previewFolder, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion); + $preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion); } catch (NotFoundException $e) { if (!$this->previewManager->isMimeSupported($mimeType)) { throw new NotFoundException(); @@ -208,6 +210,8 @@ class Generator { } $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion); + // New file, augment our array + $previewFiles[] = $preview; } } catch (\InvalidArgumentException $e) { throw new NotFoundException("", 0, $e); @@ -233,75 +237,19 @@ class Generator { * Generate a small image straight away without generating a max preview first * Preview generated is 256x256 * + * @param ISimpleFile[] $previewFiles + * * @throws NotFoundException */ - private function getSmallImagePreview(ISimpleFolder $previewFolder, File $file, string $mimeType, string $prefix, bool $crop): ISimpleFile { - $nodes = $previewFolder->getDirectoryListing(); - - foreach ($nodes as $node) { - $name = $node->getName(); - if (($prefix === '' || str_starts_with($name, $prefix))) { - // Prefix match - if (str_starts_with($name, $prefix . '256-256-crop') && $crop) { - // Cropped image - return $node; - } - - if (str_starts_with($name, $prefix . '256-256.') && !$crop) { - // Uncropped image - return $node; - } - } - } - - $previewProviders = $this->previewManager->getProviders(); - foreach ($previewProviders as $supportedMimeType => $providers) { - // Filter out providers that does not support this mime - if (!preg_match($supportedMimeType, $mimeType)) { - continue; - } - - foreach ($providers as $providerClosure) { - $provider = $this->helper->getProvider($providerClosure); - if (!($provider instanceof IProviderV2)) { - continue; - } - - if (!$provider->isAvailable($file)) { - continue; - } - - $preview = $this->helper->getThumbnail($provider, $file, 256, 256, $crop); - - if (!($preview instanceof IImage)) { - continue; - } - - // Try to get the extension. - try { - $ext = $this->getExtention($preview->dataMimeType()); - } catch (\InvalidArgumentException $e) { - // Just continue to the next iteration if this preview doesn't have a valid mimetype - continue; - } - - $path = $this->generatePath(256, 256, $crop, $preview->dataMimeType(), $prefix); - try { - $file = $previewFolder->newFile($path); - if ($preview instanceof IStreamImage) { - $file->putContent($preview->resource()); - } else { - $file->putContent($preview->data()); - } - } catch (NotPermittedException $e) { - throw new NotFoundException(); - } + private function getSmallImagePreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, string $mimeType, string $prefix, bool $crop): ISimpleFile { + $width = 256; + $height = 256; - return $file; - } + try { + return $this->getCachedPreview($previewFiles, $width, $height, $crop, $mimeType, $prefix); + } catch (NotFoundException $e) { + return $this->generateProviderPreview($previewFolder, $file, $width, $height, $crop, false, $mimeType, $prefix); } - - throw new NotFoundException('No provider successfully handled the preview generation'); } /** @@ -398,22 +346,30 @@ class Generator { /** * @param ISimpleFolder $previewFolder + * @param ISimpleFile[] $previewFiles * @param File $file * @param string $mimeType * @param string $prefix * @return ISimpleFile * @throws NotFoundException */ - private function getMaxPreview(ISimpleFolder $previewFolder, File $file, $mimeType, $prefix) { - $nodes = $previewFolder->getDirectoryListing(); - - foreach ($nodes as $node) { + private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) { + // We don't know the max preview size, so we can't use getCachedPreview. + // It might have been generated with a higher resolution than the current value. + foreach ($previewFiles as $node) { $name = $node->getName(); if (($prefix === '' || strpos($name, $prefix) === 0) && strpos($name, 'max')) { return $node; } } + $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096); + $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096); + + return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix); + } + + private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) { $previewProviders = $this->previewManager->getProviders(); foreach ($previewProviders as $supportedMimeType => $providers) { // Filter out providers that does not support this mime @@ -431,13 +387,10 @@ class Generator { continue; } - $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096); - $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096); - $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); try { - $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight); + $preview = $this->helper->getThumbnail($provider, $file, $width, $height); } finally { self::unguardWithSemaphore($sem); } @@ -446,15 +399,7 @@ class Generator { continue; } - // Try to get the extention. - try { - $ext = $this->getExtention($preview->dataMimeType()); - } catch (\InvalidArgumentException $e) { - // Just continue to the next iteration if this preview doesn't have a valid mimetype - continue; - } - - $path = $prefix . (string)$preview->width() . '-' . (string)$preview->height() . '-max.' . $ext; + $path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix); try { $file = $previewFolder->newFile($path); if ($preview instanceof IStreamImage) { @@ -470,7 +415,7 @@ class Generator { } } - throw new NotFoundException(); + throw new NotFoundException('No provider successfully handled the preview generation'); } /** @@ -487,15 +432,19 @@ class Generator { * @param int $width * @param int $height * @param bool $crop + * @param bool $max * @param string $mimeType * @param string $prefix * @return string */ - private function generatePath($width, $height, $crop, $mimeType, $prefix) { + private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) { $path = $prefix . (string)$width . '-' . (string)$height; if ($crop) { $path .= '-crop'; } + if ($max) { + $path .= '-max'; + } $ext = $this->getExtention($mimeType); $path .= '.' . $ext; @@ -637,7 +586,8 @@ class Generator { self::unguardWithSemaphore($sem); } - $path = $this->generatePath($width, $height, $crop, $preview->dataMimeType(), $prefix); + + $path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix); try { $file = $previewFolder->newFile($path); $file->putContent($preview->data()); @@ -649,7 +599,7 @@ class Generator { } /** - * @param ISimpleFolder $previewFolder + * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing() * @param int $width * @param int $height * @param bool $crop @@ -659,10 +609,14 @@ class Generator { * * @throws NotFoundException */ - private function getCachedPreview(ISimpleFolder $previewFolder, $width, $height, $crop, $mimeType, $prefix) { - $path = $this->generatePath($width, $height, $crop, $mimeType, $prefix); - - return $previewFolder->getFile($path); + private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) { + $path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix); + foreach ($files as $file) { + if ($file->getName() === $path) { + return $file; + } + } + throw new NotFoundException(); } /** diff --git a/lib/private/Session/Internal.php b/lib/private/Session/Internal.php index 87dd5ed6014..cae139018f8 100644 --- a/lib/private/Session/Internal.php +++ b/lib/private/Session/Internal.php @@ -107,6 +107,7 @@ class Internal extends Session { $this->reopen(); $this->invoke('session_unset'); $this->regenerateId(); + $this->invoke('session_write_close'); $this->startSession(true); $_SESSION = []; } diff --git a/lib/public/AppFramework/Db/IMapperException.php b/lib/public/AppFramework/Db/IMapperException.php index a4af3cfa925..5381579d2a3 100644 --- a/lib/public/AppFramework/Db/IMapperException.php +++ b/lib/public/AppFramework/Db/IMapperException.php @@ -29,5 +29,5 @@ namespace OCP\AppFramework\Db; /** * @since 16.0.0 */ -interface IMapperException { +interface IMapperException extends \Throwable { } diff --git a/tests/lib/Preview/GeneratorTest.php b/tests/lib/Preview/GeneratorTest.php index 1f6f43dce1e..37fc3935139 100644 --- a/tests/lib/Preview/GeneratorTest.php +++ b/tests/lib/Preview/GeneratorTest.php @@ -105,15 +105,12 @@ class GeneratorTest extends \Test\TestCase { $maxPreview->method('getMimeType') ->willReturn('image/png'); - $previewFolder->method('getDirectoryListing') - ->willReturn([$maxPreview]); - $previewFile = $this->createMock(ISimpleFile::class); $previewFile->method('getSize')->willReturn(1000); + $previewFile->method('getName')->willReturn('256-256.png'); - $previewFolder->method('getFile') - ->with($this->equalTo('256-256.png')) - ->willReturn($previewFile); + $previewFolder->method('getDirectoryListing') + ->willReturn([$maxPreview, $previewFile]); $this->legacyEventDispatcher->expects($this->once()) ->method('dispatch') @@ -344,14 +341,12 @@ class GeneratorTest extends \Test\TestCase { $maxPreview->method('getMimeType') ->willReturn('image/png'); - $previewFolder->method('getDirectoryListing') - ->willReturn([$maxPreview]); - $preview = $this->createMock(ISimpleFile::class); $preview->method('getSize')->willReturn(1000); - $previewFolder->method('getFile') - ->with($this->equalTo('1024-512-crop.png')) - ->willReturn($preview); + $preview->method('getName')->willReturn('1024-512-crop.png'); + + $previewFolder->method('getDirectoryListing') + ->willReturn([$maxPreview, $preview]); $this->previewManager->expects($this->never()) ->method('isMimeSupported'); |