diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php index c3f06f95783a0..94e3978e67d1a 100644 --- a/apps/dav/lib/Connector/Sabre/Principal.php +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -270,6 +270,8 @@ protected function searchUserPrincipals(array $searchProperties, $test = 'allof' $limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups(); $limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone(); $allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch(); + $ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName(); + $matchEmail = $this->shareManager->matchEmail(); // If sharing is restricted to group members only, // return only members that have groups in common @@ -298,7 +300,7 @@ protected function searchUserPrincipals(array $searchProperties, $test = 'allof' switch ($prop) { case '{http://sabredav.org/ns}email-address': if (!$allowEnumeration) { - if ($allowEnumerationFullMatch) { + if ($allowEnumerationFullMatch && $matchEmail) { $users = $this->userManager->getByEmail($value); } else { $users = []; @@ -349,8 +351,9 @@ protected function searchUserPrincipals(array $searchProperties, $test = 'allof' if ($allowEnumerationFullMatch) { $lowerSearch = strtolower($value); $users = $this->userManager->searchDisplayName($value, $searchLimit); - $users = \array_filter($users, static function (IUser $user) use ($lowerSearch) { - return strtolower($user->getDisplayName()) === $lowerSearch; + $users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) { + $lowerDisplayName = strtolower($user->getDisplayName()); + return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch); }); } else { $users = []; diff --git a/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php b/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php index d7c074c9e3b45..86413e4a366a5 100644 --- a/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php @@ -662,6 +662,10 @@ public function testSearchPrincipalWithEnumerationDisabledEmail(): void { ->method('allowEnumerationFullMatch') ->willReturn(true); + $this->shareManager->expects($this->once()) + ->method('matchEmail') + ->willReturn(true); + $user2 = $this->createMock(IUser::class); $user2->method('getUID')->willReturn('user2'); $user2->method('getDisplayName')->willReturn('User 2'); diff --git a/apps/settings/lib/Settings/Admin/Sharing.php b/apps/settings/lib/Settings/Admin/Sharing.php index 15f74c40e1e2b..58b3576e55bee 100644 --- a/apps/settings/lib/Settings/Admin/Sharing.php +++ b/apps/settings/lib/Settings/Admin/Sharing.php @@ -84,6 +84,9 @@ public function getForm() { 'restrictUserEnumerationToGroup' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no'), 'restrictUserEnumerationToPhone' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no'), 'restrictUserEnumerationFullMatch' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes'), + 'restrictUserEnumerationFullMatchUserId' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes'), + 'restrictUserEnumerationFullMatchEmail' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes'), + 'restrictUserEnumerationFullMatchIgnoreSecondDisplayName' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name', 'no'), 'enforceLinkPassword' => Util::isPublicLinkPasswordRequired(), 'onlyShareWithGroupMembers' => $this->shareManager->shareWithGroupMembersOnly(), 'shareAPIEnabled' => $this->config->getAppValue('core', 'shareapi_enabled', 'yes'), diff --git a/apps/settings/tests/Settings/Admin/SharingTest.php b/apps/settings/tests/Settings/Admin/SharingTest.php index c90429f6dd848..d352636dbcc00 100644 --- a/apps/settings/tests/Settings/Admin/SharingTest.php +++ b/apps/settings/tests/Settings/Admin/SharingTest.php @@ -82,6 +82,9 @@ public function testGetFormWithoutExcludedGroups(): void { ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], ['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name', 'no', 'no'], ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_default_expire_date', 'no', 'no'], ['core', 'shareapi_expire_after_n_days', '7', '7'], @@ -115,6 +118,9 @@ public function testGetFormWithoutExcludedGroups(): void { 'restrictUserEnumerationToGroup' => 'no', 'restrictUserEnumerationToPhone' => 'no', 'restrictUserEnumerationFullMatch' => 'yes', + 'restrictUserEnumerationFullMatchUserId' => 'yes', + 'restrictUserEnumerationFullMatchEmail' => 'yes', + 'restrictUserEnumerationFullMatchIgnoreSecondDisplayName' => 'no', 'enforceLinkPassword' => false, 'onlyShareWithGroupMembers' => false, 'shareAPIEnabled' => 'yes', @@ -154,6 +160,9 @@ public function testGetFormWithExcludedGroups(): void { ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], ['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name', 'no', 'no'], ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_default_expire_date', 'no', 'no'], ['core', 'shareapi_expire_after_n_days', '7', '7'], @@ -187,6 +196,9 @@ public function testGetFormWithExcludedGroups(): void { 'restrictUserEnumerationToGroup' => 'no', 'restrictUserEnumerationToPhone' => 'no', 'restrictUserEnumerationFullMatch' => 'yes', + 'restrictUserEnumerationFullMatchUserId' => 'yes', + 'restrictUserEnumerationFullMatchEmail' => 'yes', + 'restrictUserEnumerationFullMatchIgnoreSecondDisplayName' => 'no', 'enforceLinkPassword' => false, 'onlyShareWithGroupMembers' => false, 'shareAPIEnabled' => 'yes', diff --git a/build/integration/features/bootstrap/CollaborationContext.php b/build/integration/features/bootstrap/CollaborationContext.php index 4ac3b6e39717e..8f13e3f02f1c8 100644 --- a/build/integration/features/bootstrap/CollaborationContext.php +++ b/build/integration/features/bootstrap/CollaborationContext.php @@ -69,6 +69,9 @@ protected function resetAppConfigs(): void { $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_group'); $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_phone'); $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_userid'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_email'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name'); $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members'); } diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php index f588bc4fd73a7..762d71baab399 100644 --- a/lib/private/Collaboration/Collaborators/MailPlugin.php +++ b/lib/private/Collaboration/Collaborators/MailPlugin.php @@ -51,6 +51,8 @@ class MailPlugin implements ISearchPlugin { protected $shareeEnumerationPhone; /* @var bool */ protected $shareeEnumerationFullMatch; + /* @var bool */ + protected $shareeEnumerationFullMatchEmail; /** @var IManager */ private $contactsManager; @@ -88,12 +90,17 @@ public function __construct(IManager $contactsManager, $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; $this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; + $this->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes'; } /** * {@inheritdoc} */ public function search($search, $limit, $offset, ISearchResult $searchResult) { + if ($this->shareeEnumerationFullMatch && !$this->shareeEnumerationFullMatchEmail) { + return false; + } + $currentUserId = $this->userSession->getUser()->getUID(); $result = $userResults = ['wide' => [], 'exact' => []]; diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index 9ed94082f0d0e..49b7a3caac412 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -54,6 +54,12 @@ class UserPlugin implements ISearchPlugin { protected $shareeEnumerationPhone; /* @var bool */ protected $shareeEnumerationFullMatch; + /* @var bool */ + protected $shareeEnumerationFullMatchUserId; + /* @var bool */ + protected $shareeEnumerationFullMatchEmail; + /* @var bool */ + protected $shareeEnumerationFullMatchIgnoreSecondDisplayName; /** @var IConfig */ private $config; @@ -87,6 +93,9 @@ public function __construct(IConfig $config, $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; $this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; + $this->shareeEnumerationFullMatchUserId = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes') === 'yes'; + $this->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes'; + $this->shareeEnumerationFullMatchIgnoreSecondDisplayName = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name', 'no') === 'yes'; } public function search($search, $limit, $offset, ISearchResult $searchResult) { @@ -178,7 +187,8 @@ public function search($search, $limit, $offset, ISearchResult $searchResult) { $this->shareeEnumerationFullMatch && $lowerSearch !== '' && (strtolower($uid) === $lowerSearch || strtolower($userDisplayName) === $lowerSearch || - strtolower($userEmail) === $lowerSearch) + ($this->shareeEnumerationFullMatchIgnoreSecondDisplayName && trim(strtolower(preg_replace('/ \(.*\)$/', '', $userDisplayName))) === $lowerSearch) || + ($this->shareeEnumerationFullMatchEmail && strtolower($userEmail) === $lowerSearch)) ) { if (strtolower($uid) === $lowerSearch) { $foundUserById = true; @@ -228,7 +238,7 @@ public function search($search, $limit, $offset, ISearchResult $searchResult) { } } - if ($this->shareeEnumerationFullMatch && $offset === 0 && !$foundUserById) { + if ($this->shareeEnumerationFullMatch && $this->shareeEnumerationFullMatchUserId && $offset === 0 && !$foundUserById) { // On page one we try if the search result has a direct hit on the // user id and if so, we add that to the exact match list $user = $this->userManager->get($search); diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 1891e3a128337..0bf9093eca6bf 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -1915,6 +1915,14 @@ public function allowEnumerationFullMatch(): bool { return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; } + public function matchEmail(): bool { + return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes'; + } + + public function ignoreSecondDisplayName(): bool { + return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name', 'no') === 'yes'; + } + public function currentUserCanEnumerateTargetUser(?IUser $currentUser, IUser $targetUser): bool { if ($this->allowEnumerationFullMatch()) { return true; diff --git a/lib/public/Share/IManager.php b/lib/public/Share/IManager.php index 8b1f5144b9aa3..b2b54d77eca97 100644 --- a/lib/public/Share/IManager.php +++ b/lib/public/Share/IManager.php @@ -448,6 +448,22 @@ public function limitEnumerationToPhone(): bool; */ public function allowEnumerationFullMatch(): bool; + /** + * Check if the search should match the email + * + * @return bool + * @since 25.0.0 + */ + public function matchEmail(): bool; + + /** + * Check if the search should ignore the second in parentheses display name if there is any + * + * @return bool + * @since 25.0.0 + */ + public function ignoreSecondDisplayName(): bool; + /** * Check if the current user can enumerate the target user * diff --git a/tests/lib/Collaboration/Collaborators/UserPluginTest.php b/tests/lib/Collaboration/Collaborators/UserPluginTest.php index acbcd42f04f75..2b7a08fe4e1ac 100644 --- a/tests/lib/Collaboration/Collaborators/UserPluginTest.php +++ b/tests/lib/Collaboration/Collaborators/UserPluginTest.php @@ -104,21 +104,12 @@ public function instantiatePlugin() { ); } - public function mockConfig($shareWithGroupOnly, $shareeEnumeration, $shareeEnumerationLimitToGroup, $shareeEnumerationPhone = false) { + public function mockConfig($mockedSettings) { $this->config->expects($this->any()) ->method('getAppValue') ->willReturnCallback( - function ($appName, $key, $default) use ($shareWithGroupOnly, $shareeEnumeration, $shareeEnumerationLimitToGroup, $shareeEnumerationPhone) { - if ($appName === 'core' && $key === 'shareapi_only_share_with_group_members') { - return $shareWithGroupOnly ? 'yes' : 'no'; - } elseif ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { - return $shareeEnumeration ? 'yes' : 'no'; - } elseif ($appName === 'core' && $key === 'shareapi_restrict_user_enumeration_to_group') { - return $shareeEnumerationLimitToGroup ? 'yes' : 'no'; - } elseif ($appName === 'core' && $key === 'shareapi_restrict_user_enumeration_to_phone') { - return $shareeEnumerationPhone ? 'yes' : 'no'; - } - return $default; + function ($appName, $key, $default) use ($mockedSettings) { + return $mockedSettings[$appName][$key] ?? $default; } ); } @@ -470,7 +461,13 @@ public function testSearch( array $users = [], $shareeEnumerationPhone = false ) { - $this->mockConfig($shareWithGroupOnly, $shareeEnumeration, false, $shareeEnumerationPhone); + $this->mockConfig(["core" => [ + 'shareapi_only_share_with_group_members' => $shareWithGroupOnly ? 'yes' : 'no', + 'shareapi_allow_share_dialog_user_enumeration' => $shareeEnumeration? 'yes' : 'no', + 'shareapi_restrict_user_enumeration_to_group' => false ? 'yes' : 'no', + 'shareapi_restrict_user_enumeration_to_phone' => $shareeEnumerationPhone ? 'yes' : 'no', + ]]); + $this->instantiatePlugin(); $this->session->expects($this->any()) @@ -586,6 +583,83 @@ public function dataSearchEnumeration() { ['uid' => 'test2', 'groups' => ['groupB']], ], ['exact' => [], 'wide' => ['test1']], + ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], + ], + [ + 'test', + ['groupA'], + [ + ['uid' => 'test1', 'displayName' => 'Test user 1', 'groups' => ['groupA']], + ['uid' => 'test2', 'displayName' => 'Test user 2', 'groups' => ['groupA']], + ], + ['exact' => [], 'wide' => []], + ['core' => ['shareapi_allow_share_dialog_user_enumeration' => 'no']], + ], + [ + 'test1', + ['groupA'], + [ + ['uid' => 'test1', 'displayName' => 'Test user 1', 'groups' => ['groupA']], + ['uid' => 'test2', 'displayName' => 'Test user 2', 'groups' => ['groupA']], + ], + ['exact' => ['test1'], 'wide' => []], + ['core' => ['shareapi_allow_share_dialog_user_enumeration' => 'no']], + ], + [ + 'test1', + ['groupA'], + [ + ['uid' => 'test1', 'displayName' => 'Test user 1', 'groups' => ['groupA']], + ['uid' => 'test2', 'displayName' => 'Test user 2', 'groups' => ['groupA']], + ], + ['exact' => [], 'wide' => []], + [ + 'core' => [ + 'shareapi_allow_share_dialog_user_enumeration' => 'no', + 'shareapi_restrict_user_enumeration_full_match_userid' => 'no', + ], + ] + ], + [ + 'Test user 1', + ['groupA'], + [ + ['uid' => 'test1', 'displayName' => 'Test user 1', 'groups' => ['groupA']], + ['uid' => 'test2', 'displayName' => 'Test user 2', 'groups' => ['groupA']], + ], + ['exact' => ['test1'], 'wide' => []], + [ + 'core' => [ + 'shareapi_allow_share_dialog_user_enumeration' => 'no', + 'shareapi_restrict_user_enumeration_full_match_userid' => 'no', + ], + ] + ], + [ + 'Test user 1', + ['groupA'], + [ + ['uid' => 'test1', 'displayName' => 'Test user 1 (Second displayName for user 1)', 'groups' => ['groupA']], + ['uid' => 'test2', 'displayName' => 'Test user 2 (Second displayName for user 2)', 'groups' => ['groupA']], + ], + ['exact' => [], 'wide' => []], + ['core' => ['shareapi_allow_share_dialog_user_enumeration' => 'no'], + ] + ], + [ + 'Test user 1', + ['groupA'], + [ + ['uid' => 'test1', 'displayName' => 'Test user 1 (Second displayName for user 1)', 'groups' => ['groupA']], + ['uid' => 'test2', 'displayName' => 'Test user 2 (Second displayName for user 2)', 'groups' => ['groupA']], + ], + ['exact' => ['test1'], 'wide' => []], + [ + 'core' => [ + 'shareapi_allow_share_dialog_user_enumeration' => 'no', + 'shareapi_restrict_user_enumeration_full_match_ignore_second_display_name' => 'yes', + ], + ] ], [ 'test1', @@ -595,6 +669,7 @@ public function dataSearchEnumeration() { ['uid' => 'test2', 'groups' => ['groupB']], ], ['exact' => ['test1'], 'wide' => []], + ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], ], [ 'test', @@ -604,6 +679,7 @@ public function dataSearchEnumeration() { ['uid' => 'test2', 'groups' => ['groupB', 'groupA']], ], ['exact' => [], 'wide' => ['test1', 'test2']], + ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], ], [ 'test', @@ -613,6 +689,7 @@ public function dataSearchEnumeration() { ['uid' => 'test2', 'groups' => ['groupB', 'groupA']], ], ['exact' => [], 'wide' => ['test1', 'test2']], + ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], ], [ 'test', @@ -622,6 +699,7 @@ public function dataSearchEnumeration() { ['uid' => 'test2', 'groups' => ['groupB', 'groupA']], ], ['exact' => [], 'wide' => ['test1', 'test2']], + ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], ], [ 'test', @@ -631,6 +709,7 @@ public function dataSearchEnumeration() { ['uid' => 'test2', 'groups' => ['groupB', 'groupA']], ], ['exact' => [], 'wide' => []], + ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], ], [ 'test', @@ -640,6 +719,7 @@ public function dataSearchEnumeration() { ['uid' => 'test2', 'groups' => []], ], ['exact' => [], 'wide' => []], + ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], ], [ 'test', @@ -649,6 +729,7 @@ public function dataSearchEnumeration() { ['uid' => 'test2', 'groups' => []], ], ['exact' => [], 'wide' => []], + ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], ], ]; } @@ -656,19 +737,38 @@ public function dataSearchEnumeration() { /** * @dataProvider dataSearchEnumeration */ - public function testSearchEnumerationLimit($search, $userGroups, $matchingUsers, $result) { - $this->mockConfig(false, true, true); + public function testSearchEnumerationLimit($search, $userGroups, $matchingUsers, $result, $mockedSettings) { + $this->mockConfig($mockedSettings); $userResults = []; foreach ($matchingUsers as $user) { $userResults[$user['uid']] = $user['uid']; } - $mappedResultExact = array_map(function ($user) { - return ['label' => $user, 'value' => ['shareType' => 0, 'shareWith' => $user], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => $user]; + $usersById = []; + foreach ($matchingUsers as $user) { + $usersById[$user['uid']] = $user; + } + + $mappedResultExact = array_map(function ($user) use ($usersById, $search) { + return [ + 'label' => $search === $user ? $user : $usersById[$user]['displayName'], + 'value' => ['shareType' => 0, 'shareWith' => $user], + 'icon' => 'icon-user', + 'subline' => null, + 'status' => [], + 'shareWithDisplayNameUnique' => $user, + ]; }, $result['exact']); $mappedResultWide = array_map(function ($user) { - return ['label' => $user, 'value' => ['shareType' => 0, 'shareWith' => $user], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => $user]; + return [ + 'label' => $user, + 'value' => ['shareType' => 0, 'shareWith' => $user], + 'icon' => 'icon-user', + 'subline' => null, + 'status' => [], + 'shareWithDisplayNameUnique' => $user, + ]; }, $result['wide']); $this->userManager @@ -679,6 +779,21 @@ public function testSearchEnumerationLimit($search, $userGroups, $matchingUsers, } return null; }); + $this->userManager + ->method('searchDisplayName') + ->willReturnCallback(function ($search) use ($matchingUsers) { + $users = array_filter( + $matchingUsers, + function ($user) use ($search) { + return str_contains(strtolower($user['displayName']), strtolower($search)); + }); + return array_map( + function ($user) { + return $this->getUserMock($user['uid'], $user['displayName']); + }, + $users + ); + }); $this->groupManager->method('displayNamesInGroup') ->willReturn($userResults);