diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php index e270b579e1fa7..8902156afe186 100644 --- a/apps/dav/lib/CardDAV/AddressBookImpl.php +++ b/apps/dav/lib/CardDAV/AddressBookImpl.php @@ -107,6 +107,8 @@ public function getDisplayName() { * - 'escape_like_param' - If set to false wildcards _ and % are not escaped * - 'limit' - Set a numeric limit for the search results * - 'offset' - Set the offset for the limited search results + * - 'wildcard' - Whether the search should use wildcards + * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs * example result: * [ diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index d5c36096956dd..0696fb2f5fb98 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -1004,6 +1004,8 @@ public function updateShares(IShareable $shareable, $add, $remove) { * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are * - 'limit' - Set a numeric limit for the search results * - 'offset' - Set the offset for the limited search results + * - 'wildcard' - Whether the search should use wildcards + * @psalm-param array{escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs */ public function search($addressBookId, $pattern, $searchProperties, $options = []): array { @@ -1035,6 +1037,7 @@ public function searchPrincipalUri(string $principalUri, * @param string $pattern * @param array $searchProperties * @param array $options + * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options * @return array */ private function searchByAddressBookIds(array $addressBookIds, @@ -1042,6 +1045,7 @@ private function searchByAddressBookIds(array $addressBookIds, array $searchProperties, array $options = []): array { $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false; + $useWildcards = !\array_key_exists('wildcard', $options) || $options['wildcard'] !== false; $query2 = $this->db->getQueryBuilder(); @@ -1083,7 +1087,9 @@ private function searchByAddressBookIds(array $addressBookIds, // No need for like when the pattern is empty if ('' !== $pattern) { - if (!$escapePattern) { + if (!$useWildcards) { + $query2->andWhere($query2->expr()->eq('cp.value', $query2->createNamedParameter($pattern))); + } elseif (!$escapePattern) { $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern))); } else { $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); diff --git a/apps/federatedfilesharing/lib/Notifier.php b/apps/federatedfilesharing/lib/Notifier.php index f365a14a13ca3..3f6d22d03aaf0 100644 --- a/apps/federatedfilesharing/lib/Notifier.php +++ b/apps/federatedfilesharing/lib/Notifier.php @@ -255,7 +255,12 @@ protected function getDisplayNameFromContact($federatedCloudId) { } } - $addressBookEntries = $this->contactsManager->search($federatedCloudId, ['CLOUD']); + $addressBookEntries = $this->contactsManager->search($federatedCloudId, ['CLOUD'], [ + 'limit' => 1, + 'enumeration' => false, + 'fullmatch' => false, + 'strict_search' => true, + ]); foreach ($addressBookEntries as $entry) { if (isset($entry['CLOUD'])) { foreach ($entry['CLOUD'] as $cloudID) { diff --git a/apps/files/lib/Activity/Provider.php b/apps/files/lib/Activity/Provider.php index f50d9d6a42a6b..6d5618c4128ce 100644 --- a/apps/files/lib/Activity/Provider.php +++ b/apps/files/lib/Activity/Provider.php @@ -560,7 +560,12 @@ protected function getDisplayNameFromAddressBook(string $search): string { return $this->displayNames[$search]; } - $addressBookContacts = $this->contactsManager->search($search, ['CLOUD']); + $addressBookContacts = $this->contactsManager->search($search, ['CLOUD'], [ + 'limit' => 1, + 'enumeration' => false, + 'fullmatch' => false, + 'strict_search' => true, + ]); foreach ($addressBookContacts as $contact) { if (isset($contact['isLocalSystemBook'])) { continue; diff --git a/apps/files_sharing/lib/Activity/Providers/Base.php b/apps/files_sharing/lib/Activity/Providers/Base.php index 843a0c447f161..b8fee9f66f67b 100644 --- a/apps/files_sharing/lib/Activity/Providers/Base.php +++ b/apps/files_sharing/lib/Activity/Providers/Base.php @@ -204,7 +204,12 @@ protected function getDisplayNameFromAddressBook(string $search): string { return $this->displayNames[$search]; } - $addressBookContacts = $this->contactsManager->search($search, ['CLOUD']); + $addressBookContacts = $this->contactsManager->search($search, ['CLOUD'], [ + 'limit' => 1, + 'enumeration' => false, + 'fullmatch' => false, + 'strict_search' => true, + ]); foreach ($addressBookContacts as $contact) { if (isset($contact['isLocalSystemBook'])) { continue; diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index 8a3a8ab038f22..108014923d5a5 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -333,8 +333,12 @@ protected function formatShare(IShare $share, Node $recipientNode = null): array * @return string */ private function getDisplayNameFromAddressBook(string $query, string $property): string { - // FIXME: If we inject the contacts manager it gets initialized bofore any address books are registered - $result = \OC::$server->getContactsManager()->search($query, [$property]); + // FIXME: If we inject the contacts manager it gets initialized before any address books are registered + $result = \OC::$server->getContactsManager()->search($query, [$property], [ + 'limit' => 1, + 'enumeration' => false, + 'strict_search' => true, + ]); foreach ($result as $r) { foreach ($r[$property] as $value) { if ($value === $query && $r['FN']) { diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 804a98f6d3fdf..5b45adc0a4c1e 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -4227,7 +4227,11 @@ public function testFormatShare(array $expects, \OCP\Share\IShare $share, array $cm->method('search') ->willReturnMap([ - ['user@server.com', ['CLOUD'], [], + ['user@server.com', ['CLOUD'], [ + 'limit' => 1, + 'enumeration' => false, + 'strict_search' => true, + ], [ [ 'CLOUD' => [ @@ -4237,7 +4241,11 @@ public function testFormatShare(array $expects, \OCP\Share\IShare $share, array ], ], ], - ['user@server.com', ['EMAIL'], [], + ['user@server.com', ['EMAIL'], [ + 'limit' => 1, + 'enumeration' => false, + 'strict_search' => true, + ], [ [ 'EMAIL' => [ diff --git a/apps/sharebymail/lib/Activity.php b/apps/sharebymail/lib/Activity.php index 23f036c470003..861969fca8ac6 100644 --- a/apps/sharebymail/lib/Activity.php +++ b/apps/sharebymail/lib/Activity.php @@ -363,7 +363,12 @@ protected function generateUserParameter($uid) { * @return string */ protected function getContactName($email) { - $addressBookContacts = $this->contactsManager->search($email, ['EMAIL']); + $addressBookContacts = $this->contactsManager->search($email, ['EMAIL'], [ + 'limit' => 1, + 'enumeration' => false, + 'fullmatch' => false, + 'strict_search' => true, + ]); foreach ($addressBookContacts as $contact) { if (isset($contact['isLocalSystemBook'])) { diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php index 240e16374d54c..301d378098705 100644 --- a/lib/private/Collaboration/Collaborators/MailPlugin.php +++ b/lib/private/Collaboration/Collaborators/MailPlugin.php @@ -87,12 +87,7 @@ public function __construct(IManager $contactsManager, } /** - * @param $search - * @param $limit - * @param $offset - * @param ISearchResult $searchResult - * @return bool - * @since 13.0.0 + * {@inheritdoc} */ public function search($search, $limit, $offset, ISearchResult $searchResult) { $currentUserId = $this->userSession->getUser()->getUID(); @@ -102,7 +97,16 @@ public function search($search, $limit, $offset, ISearchResult $searchResult) { $emailType = new SearchResultType('emails'); // Search in contacts - $addressBookContacts = $this->contactsManager->search($search, ['EMAIL', 'FN'], ['limit' => $limit, 'offset' => $offset]); + $addressBookContacts = $this->contactsManager->search( + $search, + ['EMAIL', 'FN'], + [ + 'limit' => $limit, + 'offset' => $offset, + 'enumeration' => (bool) $this->shareeEnumeration, + 'fullmatch' => (bool) $this->shareeEnumerationFullMatch, + ] + ); $lowerSearch = strtolower($search); foreach ($addressBookContacts as $contact) { if (isset($contact['EMAIL'])) { diff --git a/lib/private/Collaboration/Collaborators/RemotePlugin.php b/lib/private/Collaboration/Collaborators/RemotePlugin.php index 3d9b1f9847ac8..e053465e83dfc 100644 --- a/lib/private/Collaboration/Collaborators/RemotePlugin.php +++ b/lib/private/Collaboration/Collaborators/RemotePlugin.php @@ -68,7 +68,12 @@ public function search($search, $limit, $offset, ISearchResult $searchResult) { $resultType = new SearchResultType('remotes'); // Search in contacts - $addressBookContacts = $this->contactsManager->search($search, ['CLOUD', 'FN'], ['limit' => $limit, 'offset' => $offset]); + $addressBookContacts = $this->contactsManager->search($search, ['CLOUD', 'FN'], [ + 'limit' => $limit, + 'offset' => $offset, + 'enumeration' => false, + 'fullmatch' => false, + ]); foreach ($addressBookContacts as $contact) { if (isset($contact['isLocalSystemBook'])) { continue; diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php index 69f26c7969fb8..94531d2bf554d 100644 --- a/lib/private/Contacts/ContactsMenu/ContactsStore.php +++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php @@ -75,7 +75,10 @@ public function __construct(IManager $contactsManager, * @return IEntry[] */ public function getContacts(IUser $user, $filter, ?int $limit = null, ?int $offset = null) { - $options = []; + $options = [ + 'enumeration' => $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes', + 'fullmatch' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes', + ]; if ($limit !== null) { $options['limit'] = $limit; } @@ -244,7 +247,9 @@ public function findOne(IUser $user, $shareType, $shareWith) { } $userId = $user->getUID(); - $allContacts = $this->contactsManager->search($shareWith, $filter); + $allContacts = $this->contactsManager->search($shareWith, $filter, [ + 'strict_search' => true, + ]); $contacts = array_filter($allContacts, function ($contact) use ($userId) { return $contact['UID'] !== $userId; }); diff --git a/lib/private/ContactsManager.php b/lib/private/ContactsManager.php index 4cdc3d48fffd1..f83315ade2444 100644 --- a/lib/private/ContactsManager.php +++ b/lib/private/ContactsManager.php @@ -43,13 +43,35 @@ class ContactsManager implements IManager { * - 'escape_like_param' - If set to false wildcards _ and % are not escaped * - 'limit' - Set a numeric limit for the search results * - 'offset' - Set the offset for the limited search results + * - 'enumeration' - (since 23.0.0) Whether user enumeration on system address book is allowed + * - 'fullmatch' - (since 23.0.0) Whether matching on full detail in system address book is allowed + * - 'strict_search' - (since 23.0.0) Whether the search pattern is full string or partial search + * @psalm-param array{escape_like_param?: bool, limit?: int, offset?: int, enumeration?: bool, fullmatch?: bool, strict_search?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs */ public function search($pattern, $searchProperties = [], $options = []) { $this->loadAddressBooks(); $result = []; foreach ($this->addressBooks as $addressBook) { - $r = $addressBook->search($pattern, $searchProperties, $options); + $searchOptions = $options; + $strictSearch = array_key_exists('strict_search', $options) && $options['strict_search'] === true; + + if ($addressBook->isSystemAddressBook()) { + $fullMatch = !\array_key_exists('fullmatch', $options) || $options['fullmatch'] !== false; + if (!$fullMatch) { + // Neither full match is allowed, so skip the system address book + continue; + } + if ($strictSearch) { + $searchOptions['wildcard'] = false; + } else { + $searchOptions['wildcard'] = !\array_key_exists('enumeration', $options) || $options['enumeration'] !== false; + } + } else { + $searchOptions['wildcard'] = !$strictSearch; + } + + $r = $addressBook->search($pattern, $searchProperties, $searchOptions); $contacts = []; foreach ($r as $c) { $c['addressbook-key'] = $addressBook->getKey(); diff --git a/lib/private/Federation/CloudIdManager.php b/lib/private/Federation/CloudIdManager.php index 0671a0bfa0229..012be25b77f5f 100644 --- a/lib/private/Federation/CloudIdManager.php +++ b/lib/private/Federation/CloudIdManager.php @@ -83,7 +83,12 @@ public function resolveCloudId(string $cloudId): ICloudId { } protected function getDisplayNameFromContact(string $cloudId): ?string { - $addressBookEntries = $this->contactsManager->search($cloudId, ['CLOUD']); + $addressBookEntries = $this->contactsManager->search($cloudId, ['CLOUD'], [ + 'limit' => 1, + 'enumeration' => false, + 'fullmatch' => false, + 'strict_search' => true, + ]); foreach ($addressBookEntries as $entry) { if (isset($entry['CLOUD'])) { foreach ($entry['CLOUD'] as $cloudID) { diff --git a/lib/private/Share/Share.php b/lib/private/Share/Share.php index 2d0d4f1cf87e1..5b310ac05c92c 100644 --- a/lib/private/Share/Share.php +++ b/lib/private/Share/Share.php @@ -594,7 +594,12 @@ public static function getItems($itemType, $item = null, $shareType = null, $sha $row['share_with_displayname'] = $shareWithUser === null ? $row['share_with'] : $shareWithUser->getDisplayName(); } elseif (isset($row['share_with']) && $row['share_with'] != '' && $row['share_type'] === IShare::TYPE_REMOTE) { - $addressBookEntries = \OC::$server->getContactsManager()->search($row['share_with'], ['CLOUD']); + $addressBookEntries = \OC::$server->getContactsManager()->search($row['share_with'], ['CLOUD'], [ + 'limit' => 1, + 'enumeration' => false, + 'fullmatch' => false, + 'strict_search' => true, + ]); foreach ($addressBookEntries as $entry) { foreach ($entry['CLOUD'] as $cloudID) { if ($cloudID === $row['share_with']) { diff --git a/lib/public/Contacts/IManager.php b/lib/public/Contacts/IManager.php index eba3d604ca3b2..544eaaafee31e 100644 --- a/lib/public/Contacts/IManager.php +++ b/lib/public/Contacts/IManager.php @@ -99,6 +99,10 @@ interface IManager { * - 'escape_like_param' - If set to false wildcards _ and % are not escaped * - 'limit' - Set a numeric limit for the search results * - 'offset' - Set the offset for the limited search results + * - 'enumeration' - (since 23.0.0) Whether user enumeration on system address book is allowed + * - 'fullmatch' - (since 23.0.0) Whether matching on full detail in system addresss book is allowed + * - 'strict_search' - (since 23.0.0) Whether the search pattern is full string or partial search + * @psalm-param array{escape_like_param?: bool, limit?: int, offset?: int, enumeration?: bool, fullmatch?: bool, strict_search?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs * @since 6.0.0 */ diff --git a/lib/public/IAddressBook.php b/lib/public/IAddressBook.php index 66723b4b2ed2a..6c1e10ef1aa6e 100644 --- a/lib/public/IAddressBook.php +++ b/lib/public/IAddressBook.php @@ -73,6 +73,8 @@ public function getDisplayName(); * - 'escape_like_param' - If set to false wildcards _ and % are not escaped * - 'limit' - Set a numeric limit for the search results * - 'offset' - Set the offset for the limited search results + * - 'wildcard' - (since 23.0.0) Whether the search should use wildcards + * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs * example result: * [