diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index e49e47879b0e5..e335867a7da75 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -215,6 +215,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription private IConfig $config; private bool $legacyEndpoint; private string $dbObjectPropertiesTable = 'calendarobjects_props'; + private array $cachedObjects = []; public function __construct(IDBConnection $db, Principal $principalBackend, @@ -1117,6 +1118,10 @@ public function getDeletedCalendarObjectsByPrincipal(string $principalUri): arra * @return array|null */ public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) { + $key = $calendarId . '::' . $objectUri . '::' . $calendarType; + if (isset($this->cachedObjects[$key])) { + return $this->cachedObjects[$key]; + } $query = $this->db->getQueryBuilder(); $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) ->from('calendarobjects') @@ -1131,6 +1136,12 @@ public function getCalendarObject($calendarId, $objectUri, int $calendarType = s return null; } + $object = $this->rowToCalendarObject($row); + $this->cachedObjects[$key] = $object; + return $object; + } + + private function rowToCalendarObject(array $row): array { return [ 'id' => $row['id'], 'uri' => $row['uri'], @@ -1217,6 +1228,7 @@ public function getMultipleCalendarObjects($calendarId, array $uris, $calendarTy * @return string */ public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { + $this->cachedObjects = []; $extraData = $this->getDenormalizedData($calendarData); // Try to detect duplicates @@ -1309,6 +1321,7 @@ public function createCalendarObject($calendarId, $objectUri, $calendarData, $ca * @return string */ public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { + $this->cachedObjects = []; $extraData = $this->getDenormalizedData($calendarData); $query = $this->db->getQueryBuilder(); $query->update('calendarobjects') @@ -1359,6 +1372,7 @@ public function updateCalendarObject($calendarId, $objectUri, $calendarData, $ca * @throws Exception */ public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool { + $this->cachedObjects = []; $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId); if (empty($object)) { return false; @@ -1404,6 +1418,7 @@ public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, * @param int $classification */ public function setClassification($calendarObjectId, $classification) { + $this->cachedObjects = []; if (!in_array($classification, [ self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL ])) { @@ -1428,6 +1443,7 @@ public function setClassification($calendarObjectId, $classification) { * @return void */ public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) { + $this->cachedObjects = []; $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType); if ($data === null) { @@ -1507,6 +1523,7 @@ public function deleteCalendarObject($calendarId, $objectUri, $calendarType = se * @throws Forbidden */ public function restoreCalendarObject(array $objectData): void { + $this->cachedObjects = []; $id = (int) $objectData['id']; $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']); $targetObject = $this->getCalendarObject( @@ -1632,12 +1649,8 @@ public function calendarQuery($calendarId, array $filters, $calendarType = self: } } } - $columns = ['uri']; - if ($requirePostFilter) { - $columns = ['uri', 'calendardata']; - } $query = $this->db->getQueryBuilder(); - $query->select($columns) + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) ->from('calendarobjects') ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) @@ -1658,6 +1671,11 @@ public function calendarQuery($calendarId, array $filters, $calendarType = self: $result = []; while ($row = $stmt->fetch()) { + // if we leave it as a blob we can't read it both from the post filter and the rowToCalendarObject + if (isset($row['calendardata'])) { + $row['calendardata'] = $this->readBlob($row['calendardata']); + } + if ($requirePostFilter) { // validateFilterForObject will parse the calendar data // catch parsing errors @@ -1682,6 +1700,8 @@ public function calendarQuery($calendarId, array $filters, $calendarType = self: } } $result[] = $row['uri']; + $key = $calendarId . '::' . $row['uri'] . '::' . $calendarType; + $this->cachedObjects[$key] = $this->rowToCalendarObject($row); } return $result; @@ -2631,6 +2651,7 @@ public function getSchedulingObjects($principalUri) { * @return void */ public function deleteSchedulingObject($principalUri, $objectUri) { + $this->cachedObjects = []; $query = $this->db->getQueryBuilder(); $query->delete('schedulingobjects') ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) @@ -2647,6 +2668,7 @@ public function deleteSchedulingObject($principalUri, $objectUri) { * @return void */ public function createSchedulingObject($principalUri, $objectUri, $objectData) { + $this->cachedObjects = []; $query = $this->db->getQueryBuilder(); $query->insert('schedulingobjects') ->values([ @@ -2670,6 +2692,7 @@ public function createSchedulingObject($principalUri, $objectUri, $objectData) { * @return void */ protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) { + $this->cachedObjects = []; $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions'; $query = $this->db->getQueryBuilder(); @@ -2836,6 +2859,10 @@ public function getShares(int $resourceId): array { return $this->calendarSharingBackend->getShares($resourceId); } + public function preloadShares(array $resourceIds): void { + $this->calendarSharingBackend->preloadShares($resourceIds); + } + /** * @param boolean $value * @param \OCA\DAV\CalDAV\Calendar $calendar @@ -2905,6 +2932,7 @@ public function applyShareAcl(int $resourceId, array $acl): array { * @param int $calendarType */ public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { + $this->cachedObjects = []; $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType); try { @@ -3063,6 +3091,7 @@ protected function readCalendarData($objectData) { * @param int $objectId */ protected function purgeProperties($calendarId, $objectId) { + $this->cachedObjects = []; $query = $this->db->getQueryBuilder(); $query->delete($this->dbObjectPropertiesTable) ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId))) diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php index cd6ae1c2f7fd7..af25f6c454666 100644 --- a/apps/dav/lib/CalDAV/CalendarHome.php +++ b/apps/dav/lib/CalDAV/CalendarHome.php @@ -58,6 +58,7 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { /** @var LoggerInterface */ private $logger; + private ?array $cachedChildren = null; public function __construct(BackendInterface $caldavBackend, $principalInfo, LoggerInterface $logger) { parent::__construct($caldavBackend, $principalInfo); @@ -97,6 +98,9 @@ public function createExtendedCollection($name, MkCol $mkCol): void { * @inheritdoc */ public function getChildren() { + if ($this->cachedChildren) { + return $this->cachedChildren; + } $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); $objects = []; foreach ($calendars as $calendar) { @@ -136,6 +140,7 @@ public function getChildren() { } } + $this->cachedChildren = $objects; return $objects; } diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php index 90d2c7ebf82fd..4e17f2867678e 100644 --- a/apps/dav/lib/DAV/Sharing/Backend.php +++ b/apps/dav/lib/DAV/Sharing/Backend.php @@ -29,6 +29,7 @@ namespace OCA\DAV\DAV\Sharing; use OCA\DAV\Connector\Sabre\Principal; +use OCP\Cache\CappedMemoryCache; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUserManager; @@ -45,12 +46,15 @@ class Backend { public const ACCESS_READ_WRITE = 2; public const ACCESS_READ = 3; + private CappedMemoryCache $shareCache; + public function __construct(IDBConnection $db, IUserManager $userManager, IGroupManager $groupManager, Principal $principalBackend, string $resourceType) { $this->db = $db; $this->userManager = $userManager; $this->groupManager = $groupManager; $this->principalBackend = $principalBackend; $this->resourceType = $resourceType; + $this->shareCache = new CappedMemoryCache(); } /** @@ -58,6 +62,7 @@ public function __construct(IDBConnection $db, IUserManager $userManager, IGroup * @param list $remove */ public function updateShares(IShareable $shareable, array $add, array $remove): void { + $this->shareCache->clear(); foreach ($add as $element) { $principal = $this->principalBackend->findByUri($element['href'], ''); if ($principal !== '') { @@ -76,6 +81,7 @@ public function updateShares(IShareable $shareable, array $add, array $remove): * @param array{href: string, commonName: string, readOnly: bool} $element */ private function shareWith(IShareable $shareable, array $element): void { + $this->shareCache->clear(); $user = $element['href']; $parts = explode(':', $user, 2); if ($parts[0] !== 'principal') { @@ -119,6 +125,7 @@ private function shareWith(IShareable $shareable, array $element): void { } public function deleteAllShares(int $resourceId): void { + $this->shareCache->clear(); $query = $this->db->getQueryBuilder(); $query->delete('dav_shares') ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId))) @@ -127,6 +134,7 @@ public function deleteAllShares(int $resourceId): void { } public function deleteAllSharesByUser(string $principaluri): void { + $this->shareCache->clear(); $query = $this->db->getQueryBuilder(); $query->delete('dav_shares') ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principaluri))) @@ -135,6 +143,7 @@ public function deleteAllSharesByUser(string $principaluri): void { } private function unshare(IShareable $shareable, string $element): void { + $this->shareCache->clear(); $parts = explode(':', $element, 2); if ($parts[0] !== 'principal') { return; @@ -167,6 +176,10 @@ private function unshare(IShareable $shareable, string $element): void { * @return list */ public function getShares(int $resourceId): array { + $cached = $this->shareCache->get($resourceId); + if ($cached) { + return $cached; + } $query = $this->db->getQueryBuilder(); $result = $query->select(['principaluri', 'access']) ->from('dav_shares') @@ -188,9 +201,44 @@ public function getShares(int $resourceId): array { ]; } + $this->shareCache->set((string) $resourceId, $shares); return $shares; } + public function preloadShares(array $resourceIds): void { + $resourceIds = array_filter($resourceIds, function(int $resourceId) { + return !isset($this->shareCache[(string) $resourceId]); + }); + if (count($resourceIds) === 0) { + return; + } + $query = $this->db->getQueryBuilder(); + $result = $query->select(['resourceid', 'principaluri', 'access']) + ->from('dav_shares') + ->where($query->expr()->in('resourceid', $query->createNamedParameter($resourceIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) + ->groupBy(['principaluri', 'access', 'resourceid']) + ->executeQuery(); + + $sharesByResource = array_fill_keys($resourceIds, []); + while ($row = $result->fetch()) { + $resourceId = (int)$row['resourceid']; + $p = $this->principalBackend->getPrincipalByPath($row['principaluri']); + $sharesByResource[$resourceId][] = [ + 'href' => "principal:{$row['principaluri']}", + 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '', + 'status' => 1, + 'readOnly' => (int) $row['access'] === self::ACCESS_READ, + '{http://owncloud.org/ns}principal' => (string)$row['principaluri'], + '{http://owncloud.org/ns}group-share' => isset($p['uri']) ? str_starts_with($p['uri'], 'principals/groups') : false + ]; + } + + foreach ($resourceIds as $resourceId) { + $this->shareCache->set((string) $resourceId, $sharesByResource[$resourceId]); + } + } + /** * For shared resources the sharee is set in the ACL of the resource * diff --git a/apps/dav/lib/DAV/Sharing/Plugin.php b/apps/dav/lib/DAV/Sharing/Plugin.php index a4b2cd3681cc7..8ddcb664fd51f 100644 --- a/apps/dav/lib/DAV/Sharing/Plugin.php +++ b/apps/dav/lib/DAV/Sharing/Plugin.php @@ -24,6 +24,8 @@ */ namespace OCA\DAV\DAV\Sharing; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\CalendarHome; use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\DAV\Sharing\Xml\Invite; use OCA\DAV\DAV\Sharing\Xml\ShareRequest; @@ -201,6 +203,20 @@ public function httpPost(RequestInterface $request, ResponseInterface $response) * @return void */ public function propFind(PropFind $propFind, INode $node) { + if ($node instanceof CalendarHome && $propFind->getDepth() === 1) { + $backend = $node->getCalDAVBackend(); + if ($backend instanceof CalDavBackend) { + $calendars = $node->getChildren(); + $calendars = array_filter($calendars, function (INode $node) { + return $node instanceof IShareable; + }); + /** @var int[] $resourceIds */ + $resourceIds = array_map(function (IShareable $node) { + return $node->getResourceId(); + }, $calendars); + $backend->preloadShares($resourceIds); + } + } if ($node instanceof IShareable) { $propFind->handle('{' . Plugin::NS_OWNCLOUD . '}invite', function () use ($node) { return new Invite(