diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 105442296690d..fc04118e66a7a 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -3584,7 +3584,9 @@ private function addOwnerPrincipalToCalendar(array $calendarInfo): array { $uri = $calendarInfo['principaluri']; } - $principalInformation = $this->principalBackend->getPrincipalByPath($uri); + $principalInformation = $this->principalBackend->getPrincipalPropertiesByPath($uri, [ + '{DAV:}displayname', + ]); if (isset($principalInformation['{DAV:}displayname'])) { $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname']; } diff --git a/apps/dav/lib/CalDAV/CalendarRoot.php b/apps/dav/lib/CalDAV/CalendarRoot.php index 5e0c2d1e31a05..80a4037a3a625 100644 --- a/apps/dav/lib/CalDAV/CalendarRoot.php +++ b/apps/dav/lib/CalDAV/CalendarRoot.php @@ -9,11 +9,13 @@ use OCA\DAV\CalDAV\Federation\FederatedCalendarFactory; use OCA\DAV\CalDAV\Federation\RemoteUserCalendarHome; +use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\DAV\RemoteUserPrincipalBackend; use OCP\IConfig; use OCP\IL10N; use Psr\Log\LoggerInterface; use Sabre\CalDAV\Backend; +use Sabre\DAV\Exception\NotFound; use Sabre\DAVACL\PrincipalBackend; class CalendarRoot extends \Sabre\CalDAV\CalendarRoot { @@ -70,4 +72,25 @@ public function getName() { public function enableReturnCachedSubscriptions(string $principalUri): void { $this->returnCachedSubscriptions['principals/users/' . $principalUri] = true; } + + public function childExists($name) { + if (!($this->principalBackend instanceof Principal)) { + return parent::childExists($name); + } + + // Fetch the most shallow version of the principal just to determine if it exists + $principalInfo = $this->principalBackend->getPrincipalPropertiesByPath( + $this->principalPrefix . '/' . $name, + [], + ); + if ($principalInfo === null) { + return false; + } + + try { + return $this->getChildForPrincipal($principalInfo) !== null; + } catch (NotFound $e) { + return false; + } + } } diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php index d6ea9fd887d2e..7f6941d44cdfa 100644 --- a/apps/dav/lib/Connector/Sabre/Principal.php +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -101,6 +101,21 @@ public function getPrincipalsByPrefix($prefixPath) { * @return array */ public function getPrincipalByPath($path) { + return $this->getPrincipalPropertiesByPath($path); + } + + /** + * Returns a specific principal, specified by its path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * It is possible to optionally filter retrieved properties in case only a limited set is + * required. Note that the implementation might return more properties than requested. + * + * @param string $path The path of the principal + * @param string[]|null $propertyFilter A list of properties to be retrieved or all if null. An empty array will cause a very shallow principal to be retrieved. + */ + public function getPrincipalPropertiesByPath($path, ?array $propertyFilter = null): ?array { [$prefix, $name] = \Sabre\Uri\split($path); $decodedName = urldecode($name); @@ -127,7 +142,7 @@ public function getPrincipalByPath($path) { $user = $this->userManager->get($decodedName); if ($user !== null) { - return $this->userToPrincipal($user); + return $this->userToPrincipal($user, $propertyFilter); } } elseif ($prefix === 'principals/circles') { if ($this->userSession->getUser() !== null) { @@ -466,29 +481,44 @@ public function findByUri($uri, $principalPrefix) { /** * @param IUser $user + * @param string[]|null $propertyFilter * @return array * @throws PropertyDoesNotExistException */ - protected function userToPrincipal($user) { + protected function userToPrincipal($user, ?array $propertyFilter = null) { + $wantsProperty = static function (string $name) use ($propertyFilter) { + if ($propertyFilter === null) { + return true; + } + + return in_array($name, $propertyFilter, true); + }; + $userId = $user->getUID(); $displayName = $user->getDisplayName(); $principal = [ 'uri' => $this->principalPrefix . '/' . $userId, '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName, '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL', - '{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user), ]; - $account = $this->accountManager->getAccount($user); - $alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties()); + if ($wantsProperty('{http://nextcloud.com/ns}language')) { + $principal['{http://nextcloud.com/ns}language'] = $this->languageFactory->getUserLanguage($user); + } - $email = $user->getSystemEMailAddress(); - if (!empty($email)) { - $principal['{http://sabredav.org/ns}email-address'] = $email; + if ($wantsProperty('{http://sabredav.org/ns}email-address')) { + $email = $user->getSystemEMailAddress(); + if (!empty($email)) { + $principal['{http://sabredav.org/ns}email-address'] = $email; + } } - if (!empty($alternativeEmails)) { - $principal['{DAV:}alternate-URI-set'] = $alternativeEmails; + if ($wantsProperty('{DAV:}alternate-URI-set')) { + $account = $this->accountManager->getAccount($user); + $alternativeEmails = array_map(static fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties()); + if (!empty($alternativeEmails)) { + $principal['{DAV:}alternate-URI-set'] = $alternativeEmails; + } } return $principal; diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php index 281063bfa09b1..f1b1e3f0800db 100644 --- a/apps/dav/lib/DAV/Sharing/Backend.php +++ b/apps/dav/lib/DAV/Sharing/Backend.php @@ -139,7 +139,10 @@ public function getShares(int $resourceId): array { $rows = $this->service->getShares($resourceId); $shares = []; foreach ($rows as $row) { - $p = $this->getPrincipalByPath($row['principaluri']); + $p = $this->getPrincipalByPath($row['principaluri'], [ + 'uri', + '{DAV:}displayname', + ]); $shares[] = [ 'href' => "principal:{$row['principaluri']}", 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '', @@ -165,7 +168,10 @@ public function preloadShares(array $resourceIds): void { $sharesByResource = array_fill_keys($resourceIds, []); foreach ($rows as $row) { $resourceId = (int)$row['resourceid']; - $p = $this->getPrincipalByPath($row['principaluri']); + $p = $this->getPrincipalByPath($row['principaluri'], [ + 'uri', + '{DAV:}displayname', + ]); $sharesByResource[$resourceId][] = [ 'href' => "principal:{$row['principaluri']}", 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '', @@ -257,12 +263,15 @@ public function getSharesByShareePrincipal(string $principal): array { return $this->service->getSharesByPrincipals([$principal]); } - private function getPrincipalByPath(string $principalUri): ?array { + /** + * @param string[]|null $propertyFilter A list of properties to be retrieved or all if null. Is not guaranteed to always be applied and might overfetch. + */ + private function getPrincipalByPath(string $principalUri, ?array $propertyFilter = null): ?array { // Hacky code below ... shouldn't we check the whole (principal) root collection instead? if (str_starts_with($principalUri, RemoteUserPrincipalBackend::PRINCIPAL_PREFIX)) { return $this->remoteUserPrincipalBackend->getPrincipalByPath($principalUri); } - return $this->principalBackend->getPrincipalByPath($principalUri); + return $this->principalBackend->getPrincipalPropertiesByPath($principalUri, $propertyFilter); } } diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php index fb1eb389c61d8..4dce2d36cc51e 100644 --- a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php +++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php @@ -83,9 +83,9 @@ protected function setUp(): void { $this->createMock(IConfig::class), $this->createMock(IFactory::class) ]) - ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri']) + ->onlyMethods(['getPrincipalPropertiesByPath', 'getGroupMembership', 'findByUri']) ->getMock(); - $this->principal->expects($this->any())->method('getPrincipalByPath') + $this->principal->expects($this->any())->method('getPrincipalPropertiesByPath') ->willReturn([ 'uri' => 'principals/best-friend', '{DAV:}displayname' => 'User\'s displayname', diff --git a/apps/dav/tests/unit/DAV/Sharing/BackendTest.php b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php index d980747760b6e..d4104ee859352 100644 --- a/apps/dav/tests/unit/DAV/Sharing/BackendTest.php +++ b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php @@ -304,8 +304,8 @@ public function testGetShares(): void { ->with($resourceId) ->willReturn($rows); $this->principalBackend->expects(self::once()) - ->method('getPrincipalByPath') - ->with($principal) + ->method('getPrincipalPropertiesByPath') + ->with($principal, ['uri', '{DAV:}displayname']) ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']); $this->shareCache->expects(self::once()) ->method('set') @@ -354,8 +354,8 @@ public function testGetSharesAddressbooks(): void { ->with($resourceId) ->willReturn($rows); $this->principalBackend->expects(self::once()) - ->method('getPrincipalByPath') - ->with($principal) + ->method('getPrincipalPropertiesByPath') + ->with($principal, ['uri', '{DAV:}displayname']) ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']); $this->shareCache->expects(self::once()) ->method('set') @@ -392,7 +392,7 @@ public function testPreloadShares(): void { ->with($resourceIds) ->willReturn($rows); $this->principalBackend->expects(self::exactly(2)) - ->method('getPrincipalByPath') + ->method('getPrincipalPropertiesByPath') ->willReturnCallback(function (string $principal) use ($principalResults) { switch ($principal) { case 'principals/groups/bob': diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index b88050d2f2d29..dcc1e068c80d7 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -740,8 +740,6 @@ - circleToPrincipal($decodedName) - ?: $this->circleToPrincipal($name)]]> @@ -832,11 +830,6 @@ - - - - -