diff --git a/build/integration/collaboration_features/autocomplete.feature b/build/integration/collaboration_features/autocomplete.feature index 763b95bf1709a..e4c1455eab732 100644 --- a/build/integration/collaboration_features/autocomplete.feature +++ b/build/integration/collaboration_features/autocomplete.feature @@ -71,7 +71,6 @@ Feature: autocomplete Then get autocomplete for "autocomplete@example.com" | id | source | | autocomplete | users | - | autocomplete | users | Scenario: getting autocomplete from address book without enumeration Given As an "admin" @@ -96,7 +95,6 @@ Feature: autocomplete Then get autocomplete for "autocomplete@example.com" | id | source | | autocomplete | users | - | autocomplete | users | Scenario: getting autocomplete emails from address book with enumeration Given As an "admin" diff --git a/build/integration/features/bootstrap/ShareesContext.php b/build/integration/features/bootstrap/ShareesContext.php index 4b31d35c933cd..bc670bbb2db03 100644 --- a/build/integration/features/bootstrap/ShareesContext.php +++ b/build/integration/features/bootstrap/ShareesContext.php @@ -19,10 +19,17 @@ class ShareesContext implements Context, SnippetAcceptingContext { use AppConfiguration; protected function resetAppConfigs() { - $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members'); - $this->deleteServerConfig('core', 'shareapi_allow_share_dialog_user_enumeration'); $this->deleteServerConfig('core', 'shareapi_allow_group_sharing'); + $this->deleteServerConfig('core', 'shareapi_allow_share_dialog_user_enumeration'); $this->deleteServerConfig('core', 'shareapi_exclude_groups'); $this->deleteServerConfig('core', 'shareapi_exclude_groups_list'); + $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members'); + $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members_exclude_group_list'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_email'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_userid'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_group'); + $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_phone'); } } diff --git a/build/integration/sharees_features/sharees.feature b/build/integration/sharees_features/sharees.feature index bcfca9dbee456..d1c3c95628d62 100644 --- a/build/integration/sharees_features/sharees.feature +++ b/build/integration/sharees_features/sharees.feature @@ -291,8 +291,7 @@ Feature: sharees | itemType | file | Then the OCS status code should be "100" And the HTTP status code should be "200" - Then "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | + Then "exact users" sharees returned is empty Then "users" sharees returned is empty Then "exact groups" sharees returned is empty Then "groups" sharees returned is empty @@ -354,12 +353,9 @@ Feature: sharees | shareType | 0 | Then the OCS status code should be "100" And the HTTP status code should be "200" - # UserPlugin provides two identical results (except for the field order, but - # that is hidden by the check). # MailPlugin does not add a result if there is already one for that user. And "exact users" sharees returned are | Sharee2 | 0 | Sharee2 | sharee2@system.com | - | Sharee2 | 0 | Sharee2 | sharee2@system.com | And "users" sharees returned is empty And "exact groups" sharees returned is empty And "groups" sharees returned is empty @@ -546,11 +542,8 @@ Feature: sharees | shareTypes | 0 4 | Then the OCS status code should be "100" And the HTTP status code should be "200" - # UserPlugin provides two identical results (except for the field order, but - # that is hidden by the check) And "exact users" sharees returned are | Sharee2 | 0 | Sharee2 | sharee2@system.com | - | Sharee2 | 0 | Sharee2 | sharee2@system.com | And "users" sharees returned is empty And "exact groups" sharees returned is empty And "groups" sharees returned is empty @@ -570,11 +563,8 @@ Feature: sharees | shareTypes | 0 4 | Then the OCS status code should be "100" And the HTTP status code should be "200" - # UserPlugin provides two identical results (except for the field order, but - # that is hidden by the check) And "exact users" sharees returned are | Sharee2 | 0 | Sharee2 | sharee2@system.com | - | Sharee2 | 0 | Sharee2 | sharee2@system.com | And "users" sharees returned is empty And "exact groups" sharees returned is empty And "groups" sharees returned is empty diff --git a/build/integration/sharees_features/sharees_provisioningapiv2.feature b/build/integration/sharees_features/sharees_provisioningapiv2.feature index c5a42f7b6a802..d99bd9d69b714 100644 --- a/build/integration/sharees_features/sharees_provisioningapiv2.feature +++ b/build/integration/sharees_features/sharees_provisioningapiv2.feature @@ -189,8 +189,7 @@ Feature: sharees_provisioningapiv2 | itemType | file | Then the OCS status code should be "200" And the HTTP status code should be "200" - Then "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | + Then "exact users" sharees returned is empty Then "users" sharees returned is empty Then "exact groups" sharees returned is empty Then "groups" sharees returned is empty diff --git a/build/integration/sharees_features/sharees_user.feature b/build/integration/sharees_features/sharees_user.feature new file mode 100644 index 0000000000000..25cbd569fec24 --- /dev/null +++ b/build/integration/sharees_features/sharees_user.feature @@ -0,0 +1,497 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: sharees_user + + Background: + Given using api version "1" + + Scenario: Search for userid returns exact user + Given user "test" with displayname "Test" exists + And user "user1" exists + And As an "user1" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test | 0 | test | test | + And "users" sharees returned is empty + + Scenario: Search for userid returns exact user without sharee enumeration + Given user "test" with displayname "Test" exists + And user "user1" exists + And As an "user1" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test | 0 | test | test | + And "users" sharees returned is empty + + Scenario: Search for userid without shared group returns nothing with sharing in group only + Given user "test" with displayname "Test" exists + And group "test-group" exists + And user "test" belongs to group "test-group" + And user "user1" exists + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned is empty + + Scenario: Search for userid without shared group returns nothing with sharing in group only and without sharee enumeration + Given user "test" with displayname "Test" exists + And group "test-group" exists + And user "test" belongs to group "test-group" + And user "user1" exists + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned is empty + + Scenario: Search for userid with shared group returns exact user with sharing in group only + Given user "test" with displayname "Test" exists + And group "test-group" exists + And user "test" belongs to group "test-group" + And user "user1" exists + And user "user1" belongs to group "test-group" + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test | 0 | test | test | + And "users" sharees returned is empty + + Scenario: Search for userid with shared group returns exact user with sharing in group only and without sharee enumeration + Given user "test" with displayname "Test" exists + And group "test-group" exists + And user "test" belongs to group "test-group" + And user "user1" exists + And user "user1" belongs to group "test-group" + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test | 0 | test | test | + And "users" sharees returned is empty + + Scenario: Search for part of userid returns wide user + Given user "test1" with displayname "Test One" exists + And user "user1" exists + And As an "user1" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned are + | Test One | 0 | test1 | test1 | + + Scenario: Search for part of userid returns nothing without sharee enumeration + Given user "test1" with displayname "Test One" exists + And user "user1" exists + And As an "user1" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned is empty + + Scenario: Search for part of userid returns wide users + Given user "test1" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And user "user1" exists + And As an "user1" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned are + | Test One | 0 | test1 | test1 | + | Test Two | 0 | test2 | test2 | + + Scenario: Search for part of userid returns nothing without sharee enumeration + Given user "test1" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And user "user1" exists + And As an "user1" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned is empty + + Scenario: Search for part of displayname returns exact user and wide users + Given user "test0" with displayname "Test" exists + And user "test1" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And user "user1" exists + And As an "user1" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test | 0 | test0 | test0 | + And "users" sharees returned are + | Test One | 0 | test1 | test1 | + | Test Two | 0 | test2 | test2 | + + Scenario: Search for part of displayname returns exact user without sharee enumeration + Given user "test0" with displayname "Test" exists + And user "test1" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And user "user1" exists + And As an "user1" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test | 0 | test0 | test0 | + And "users" sharees returned is empty + + Scenario: Search for part of userid with shared group returns wide user with sharing in group only + Given user "test1" with displayname "Test One" exists + And group "abc" exists + And user "test1" belongs to group "abc" + And group "xyz" exists + And user "user1" exists + And user "user1" belongs to group "abc" + And user "user1" belongs to group "xyz" + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned are + | Test One | 0 | test1 | test1 | + + Scenario: Search for part of userid with shared group returns nothing with sharing in group only and without sharee enumeration + Given user "test1" with displayname "Test One" exists + And group "abc" exists + And user "test1" belongs to group "abc" + And group "xyz" exists + And user "user1" exists + And user "user1" belongs to group "abc" + And user "user1" belongs to group "xyz" + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned is empty + + Scenario: Search for part of userid with shared groups returns wide users with sharing in group only + Given user "test1" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And group "abc" exists + And user "test1" belongs to group "abc" + And user "test2" belongs to group "abc" + And group "xyz" exists + And user "test1" belongs to group "xyz" + And user "test2" belongs to group "xyz" + And user "user1" exists + And user "user1" belongs to group "abc" + And user "user1" belongs to group "xyz" + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned are + | Test One | 0 | test1 | test1 | + | Test Two | 0 | test2 | test2 | + + Scenario: Search for part of userid with shared groups returns nothing with sharing in group only and without sharee enumeration + Given user "test1" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And group "abc" exists + And user "test1" belongs to group "abc" + And user "test2" belongs to group "abc" + And group "xyz" exists + And user "test1" belongs to group "xyz" + And user "test2" belongs to group "xyz" + And user "user1" exists + And user "user1" belongs to group "abc" + And user "user1" belongs to group "xyz" + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned is empty + + Scenario: Search for part of userid with shared groups returns exact user and wide user with sharing in group only + Given user "test" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And group "abc" exists + And user "test" belongs to group "abc" + And group "xyz" exists + And user "test2" belongs to group "xyz" + And user "user1" exists + And user "user1" belongs to group "abc" + And user "user1" belongs to group "xyz" + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test One | 0 | test | test | + And "users" sharees returned are + | Test Two | 0 | test2 | test2 | + + Scenario: Search for part of userid with shared groups returns exact user with sharing in group only and without sharee enumeration + Given user "test" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And group "abc" exists + And user "test" belongs to group "abc" + And group "xyz" exists + And user "test2" belongs to group "xyz" + And user "user1" exists + And user "user1" belongs to group "abc" + And user "user1" belongs to group "xyz" + And As an "user1" + And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test One | 0 | test | test | + And "users" sharees returned is empty + + Scenario: Search for part of userid with shared group returns wide user with sharee enumeration limited to group + Given user "test" with displayname "foo" exists + And user "test1" exists + And user "test2" exists + And group "groupA" exists + And group "groupB" exists + And user "test" belongs to group "groupA" + And user "test1" belongs to group "groupA" + And user "test2" belongs to group "groupB" + And As an "test" + And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned are + | test1 | 0 | test1 | test1 | + + Scenario: Search for exact userid with shared group returns nothing without sharee enumeration and without full match userid enumeration + Given user "test" with displayname "foo" exists + And user "test1" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And group "groupA" exists + And user "test" belongs to group "groupA" + And user "test1" belongs to group "groupA" + And user "test2" belongs to group "groupA" + And As an "test" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + And parameter "shareapi_restrict_user_enumeration_full_match_userid" of app "core" is set to "no" + When getting sharees for + | search | test1 | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned is empty + + Scenario: Search for displayname returns exact user without sharee enumeration and without full match userid enumeration + Given user "test" with displayname "foo" exists + And user "test1" with displayname "Test One" exists + And user "test2" with displayname "Test Two" exists + And group "groupA" exists + And user "test" belongs to group "groupA" + And user "test1" belongs to group "groupA" + And user "test2" belongs to group "groupA" + And As an "test" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + And parameter "shareapi_restrict_user_enumeration_full_match_user_id" of app "core" is set to "no" + When getting sharees for + | search | Test One | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test One | 0 | test1 | test1 | + And "users" sharees returned is empty + + Scenario: Search for part of displayname returns exact user without sharee enumeration and with ignoring full match of second displayname + Given user "test" with displayname "foo" exists + And user "test1" with displayname "Test One (Second displayname for user 1)" exists + And user "test2" with displayname "Test Two (Second displayname for user 2)" exists + And group "groupA" exists + And user "test" belongs to group "groupA" + And user "test1" belongs to group "groupA" + And user "test2" belongs to group "groupA" + And As an "test" + And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" + And parameter "shareapi_restrict_user_enumeration_full_match_ignore_second_dn" of app "core" is set to "yes" + When getting sharees for + | search | Test One | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test One (Second displayname for user 1) | 0 | test1 | test1 | + And "users" sharees returned is empty + + Scenario: Search for exact userid with shared group returns exact user with sharee enumeration limited to group + Given user "test" with displayname "foo" exists + And user "test1" exists + And user "test2" exists + And group "groupA" exists + And group "groupB" exists + And user "test" belongs to group "groupA" + And user "test1" belongs to group "groupA" + And user "test2" belongs to group "groupB" + And As an "test" + And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" + When getting sharees for + | search | test1 | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | test1 | 0 | test1 | test1 | + And "users" sharees returned is empty + + Scenario: Search for part of userid with shared group returns wide user with sharee enumeration limited to group + Given user "test1" with displayname "Test One" exists + And group "test-group" exists + And user "test1" belongs to group "test-group" + And user "user1" exists + And user "user1" belongs to group "test-group" + And As an "user1" + And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned are + | Test One | 0 | test1 | test1 | + + Scenario: Search for part of userid without shared group returns nothing with sharee enumeration limited to group + Given user "test1" with displayname "Test One" exists + And user "user1" exists + And As an "user1" + And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" + When getting sharees for + | search | test | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned is empty + And "users" sharees returned is empty + + Scenario: Search for exact userid without shared group returns exact user with sharee enumeration limited to group + Given user "test1" with displayname "Test One" exists + And user "user1" exists + And As an "user1" + And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" + When getting sharees for + | search | test1 | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test One | 0 | test1 | test1 | + And "users" sharees returned is empty + + Scenario: Search for exact email without shared group returns exact user with sharee enumeration limited to group + Given user "test1" with displayname "Test One" exists + And As an "admin" + And sending "PUT" to "/cloud/users/test1" with + | key | email | + | value | test@example.com | + And user "user1" exists + And As an "user1" + And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" + When getting sharees for + | search | test@example.com | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test One | 0 | test1 | test@example.com | + And "users" sharees returned is empty + + Scenario: Search for exact additional email returns exact user + Given user "test1" with displayname "Test One" exists + And As an "admin" + And sending "PUT" to "/cloud/users/test1" with + | key | email | + | value | test@example.com | + And sending "PUT" to "/cloud/users/test1" with + | key | additional_mail | + | value | test@example.org | + And user "user1" exists + And As an "user1" + When getting sharees for + | search | test@example.org | + | itemType | file | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And "exact users" sharees returned are + | Test One (test@example.org) | 0 | test1 | test@example.org | + And "users" sharees returned is empty diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index ffad426e15dd6..19add9e1b659f 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -4,274 +4,178 @@ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OC\Collaboration\Collaborators; -use OC\KnownUser\KnownUserService; use OCP\Collaboration\Collaborators\ISearchPlugin; use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\SearchResultType; -use OCP\IConfig; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IAppConfig; +use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; use OCP\Share\IShare; use OCP\UserStatus\IManager as IUserStatusManager; +use OCP\UserStatus\IUserStatus; class UserPlugin implements ISearchPlugin { - protected bool $shareWithGroupOnly; - - protected bool $shareeEnumeration; - - protected bool $shareeEnumerationInGroupOnly; - - protected bool $shareeEnumerationPhone; - - protected bool $shareeEnumerationFullMatch; - - protected bool $shareeEnumerationFullMatchUserId; - - protected bool $shareeEnumerationFullMatchEmail; - - protected bool $shareeEnumerationFullMatchIgnoreSecondDisplayName; - public function __construct( - private IConfig $config, - private IUserManager $userManager, - private IGroupManager $groupManager, - private IUserSession $userSession, - private KnownUserService $knownUserService, - private IUserStatusManager $userStatusManager, - private mixed $shareWithGroupOnlyExcludeGroupsList = [], + private readonly IAppConfig $appConfig, + private readonly IUserManager $userManager, + private readonly IGroupManager $groupManager, + private readonly IUserSession $userSession, + private readonly IUserStatusManager $userStatusManager, + private readonly IDBConnection $connection, ) { - $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; - $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; - $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_dn', 'no') === 'yes'; - - if ($this->shareWithGroupOnly) { - $this->shareWithGroupOnlyExcludeGroupsList = json_decode($this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''), true) ?? []; - } } public function search($search, $limit, $offset, ISearchResult $searchResult): bool { - $result = ['wide' => [], 'exact' => []]; - $users = []; - $hasMoreResults = false; - - /** @var IUser */ + /** @var IUser $currentUser */ $currentUser = $this->userSession->getUser(); - $currentUserId = $currentUser->getUID(); - $currentUserGroups = $this->groupManager->getUserGroupIds($currentUser); - // ShareWithGroupOnly filtering - $currentUserGroups = array_diff($currentUserGroups, $this->shareWithGroupOnlyExcludeGroupsList); + $shareWithGroupOnlyExcludeGroupsList = json_decode($this->appConfig->getValueString('core', 'shareapi_only_share_with_group_members_exclude_group_list', '[]'), true, 512, JSON_THROW_ON_ERROR) ?? []; + $allowedGroups = array_diff($this->groupManager->getUserGroupIds($currentUser), $shareWithGroupOnlyExcludeGroupsList); - if ($this->shareWithGroupOnly || $this->shareeEnumerationInGroupOnly) { - // Search in all the groups this user is part of - foreach ($currentUserGroups as $userGroupId) { - $usersInGroup = $this->groupManager->displayNamesInGroup($userGroupId, $search, $limit, $offset); - foreach ($usersInGroup as $userId => $displayName) { - $userId = (string)$userId; - $user = $this->userManager->get($userId); - if (!$user?->isEnabled()) { - // Ignore disabled users - continue; - } - $users[$userId] = $user; - } - if (count($usersInGroup) >= $limit) { - $hasMoreResults = true; - } - } - } + /** @var array $users */ + $users = []; - // not limited to group only sharing - if (!$this->shareWithGroupOnly) { - if (!$this->shareeEnumerationPhone && !$this->shareeEnumerationInGroupOnly) { - // no restrictions, add everything - $usersTmp = $this->userManager->searchDisplayName($search, $limit, $offset); - foreach ($usersTmp as $user) { - if ($user->isEnabled()) { // Don't keep deactivated users - $users[$user->getUID()] = $user; + $shareeEnumeration = $this->appConfig->getValueString('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; + if ($shareeEnumeration) { + $shareeEnumerationRestrictToGroup = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + $shareeEnumerationRestrictToPhone = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + + if (!$shareeEnumerationRestrictToGroup && !$shareeEnumerationRestrictToPhone) { + // No restrictions, search everything. + $usersByDisplayName = $this->userManager->searchDisplayName($search, $limit, $offset); + foreach ($usersByDisplayName as $user) { + if ($user->isEnabled()) { + $users[$user->getUID()] = ['wide', $user]; } } } else { - // make sure to add phonebook matches if configured - if ($this->shareeEnumerationPhone) { - $usersTmp = $this->userManager->searchKnownUsersByDisplayName($currentUserId, $search, $limit, $offset); - foreach ($usersTmp as $user) { - if ($user->isEnabled()) { // Don't keep deactivated users - $users[$user->getUID()] = $user; + if ($shareeEnumerationRestrictToGroup) { + foreach ($allowedGroups as $groupId) { + $usersInGroup = $this->groupManager->displayNamesInGroup($groupId, $search, $limit, $offset); + foreach ($usersInGroup as $userId => $displayName) { + $userId = (string)$userId; + $user = $this->userManager->get($userId); + if ($user !== null && $user->isEnabled()) { + $users[$userId] = ['wide', $user]; + } } } } - // additionally we need to add full matches - if ($this->shareeEnumerationFullMatch) { - $usersTmp = $this->userManager->searchDisplayName($search, $limit, $offset); - foreach ($usersTmp as $user) { - if ($user->isEnabled() && mb_strtolower($user->getDisplayName()) === mb_strtolower($search)) { - $users[$user->getUID()] = $user; + if ($shareeEnumerationRestrictToPhone) { + $usersInPhonebook = $this->userManager->searchKnownUsersByDisplayName($currentUser->getUID(), $search, $limit, $offset); + foreach ($usersInPhonebook as $user) { + if ($user->isEnabled()) { + $users[$user->getUID()] = ['wide', $user]; } } } } - - uasort($users, function (IUser $a, IUser $b) { - return strcasecmp($a->getDisplayName(), $b->getDisplayName()); - }); } - $this->takeOutCurrentUser($users); - - if (!$this->shareeEnumeration || count($users) < $limit) { - $hasMoreResults = true; - } + // Even if normal sharee enumeration is not allowed, full matches are still allowed. + $shareeEnumerationFullMatch = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; + if ($shareeEnumerationFullMatch && $search !== '') { + $shareeEnumerationFullMatchUserId = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes') === 'yes'; + $shareeEnumerationFullMatchEmail = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes'; + $shareeEnumerationFullMatchIgnoreSecondDisplayName = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes'; - $foundUserById = false; - $lowerSearch = strtolower($search); - $userStatuses = $this->userStatusManager->getUserStatuses(array_keys($users)); - foreach ($users as $uid => $user) { - $userDisplayName = $user->getDisplayName(); - $userEmail = $user->getSystemEMailAddress(); - $uid = (string)$uid; + $lowerSearch = mb_strtolower($search); - $status = []; - if (array_key_exists($uid, $userStatuses)) { - $userStatus = $userStatuses[$uid]; - $status = [ - 'status' => $userStatus->getStatus(), - 'message' => $userStatus->getMessage(), - 'icon' => $userStatus->getIcon(), - 'clearAt' => $userStatus->getClearAt() - ? (int)$userStatus->getClearAt()->format('U') - : null, - ]; + // Re-use the results from earlier if possible + $usersByDisplayName ??= $this->userManager->searchDisplayName($search, $limit, $offset); + foreach ($usersByDisplayName as $user) { + if ($user->isEnabled() && (mb_strtolower($user->getDisplayName()) === $lowerSearch || ($shareeEnumerationFullMatchIgnoreSecondDisplayName && trim(mb_strtolower(preg_replace('/ \(.*\)$/', '', $user->getDisplayName()))) === $lowerSearch))) { + $users[$user->getUID()] = ['exact', $user]; + } } - - if ( - $this->shareeEnumerationFullMatch - && $lowerSearch !== '' - && ( - strtolower($uid) === $lowerSearch - || strtolower($userDisplayName) === $lowerSearch - || ($this->shareeEnumerationFullMatchIgnoreSecondDisplayName && trim(strtolower(preg_replace('/ \(.*\)$/', '', $userDisplayName))) === $lowerSearch) - || ($this->shareeEnumerationFullMatchEmail && strtolower($userEmail ?? '') === $lowerSearch) - ) - ) { - if (strtolower($uid) === $lowerSearch) { - $foundUserById = true; - } - $result['exact'][] = [ - 'label' => $userDisplayName, - 'subline' => $status['message'] ?? '', - 'icon' => 'icon-user', - 'value' => [ - 'shareType' => IShare::TYPE_USER, - 'shareWith' => $uid, - ], - 'shareWithDisplayNameUnique' => !empty($userEmail) ? $userEmail : $uid, - 'status' => $status, - ]; - } else { - $addToWideResults = false; - if ($this->shareeEnumeration - && !($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone)) { - $addToWideResults = true; + if ($shareeEnumerationFullMatchUserId) { + $user = $this->userManager->get($search); + if ($user !== null) { + $users[$user->getUID()] = ['exact', $user]; } + } - if ($this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $user->getUID())) { - $addToWideResults = true; + if ($shareeEnumerationFullMatchEmail) { + $qb = $this->connection->getQueryBuilder(); + $qb + ->select('uid', 'value', 'name') + ->from('accounts_data') + ->where($qb->expr()->eq($qb->func()->lower('value'), $qb->createNamedParameter($lowerSearch))) + ->andWhere($qb->expr()->in('name', $qb->createNamedParameter(['email', 'additional_mail'], IQueryBuilder::PARAM_STR_ARRAY))); + $result = $qb->executeQuery(); + while ($row = $result->fetch()) { + $uid = $row['uid']; + $email = $row['value']; + $isAdditional = $row['name'] === 'additional_mail'; + $users[$uid] = ['exact', $this->userManager->get($uid), $isAdditional ? $email : null]; } + $result->closeCursor(); + } + } - if (!$addToWideResults && $this->shareeEnumerationInGroupOnly) { - $commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user)); - if (!empty($commonGroups)) { - $addToWideResults = true; - } - } + uasort($users, static fn (array $a, array $b): int => strcasecmp($a[1]->getDisplayName(), $b[1]->getDisplayName())); - if ($addToWideResults) { - $result['wide'][] = [ - 'label' => $userDisplayName, - 'subline' => $status['message'] ?? '', - 'icon' => 'icon-user', - 'value' => [ - 'shareType' => IShare::TYPE_USER, - 'shareWith' => $uid, - ], - 'shareWithDisplayNameUnique' => !empty($userEmail) ? $userEmail : $uid, - 'status' => $status, - ]; - } - } + if (isset($users[$currentUser->getUID()])) { + unset($users[$currentUser->getUID()]); } - 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); - if ($user instanceof IUser) { - $addUser = true; + $shareWithGroupOnly = $this->appConfig->getValueString('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; + if ($shareWithGroupOnly) { + $users = array_filter($users, fn (array $match) => array_intersect($allowedGroups, $this->groupManager->getUserGroupIds($match[1])) !== []); + } - if ($this->shareWithGroupOnly) { - // Only add, if we have a common group - $commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user)); - $addUser = !empty($commonGroups); - } + $userStatuses = array_map( + static fn (IUserStatus $userStatus) => [ + 'status' => $userStatus->getStatus(), + 'message' => $userStatus->getMessage(), + 'icon' => $userStatus->getIcon(), + 'clearAt' => $userStatus->getClearAt() + ? (int)$userStatus->getClearAt()->format('U') + : null, + ], + $this->userStatusManager->getUserStatuses(array_keys($users)), + ); - if ($addUser) { - $status = []; - $uid = $user->getUID(); - $userEmail = $user->getSystemEMailAddress(); - if (array_key_exists($user->getUID(), $userStatuses)) { - $userStatus = $userStatuses[$user->getUID()]; - $status = [ - 'status' => $userStatus->getStatus(), - 'message' => $userStatus->getMessage(), - 'icon' => $userStatus->getIcon(), - 'clearAt' => $userStatus->getClearAt() - ? (int)$userStatus->getClearAt()->format('U') - : null, - ]; - } + $result = ['wide' => [], 'exact' => []]; + foreach ($users as $match) { + $match[2] ??= null; + [$type, $user, $uniqueDisplayName] = $match; - $result['exact'][] = [ - 'label' => $user->getDisplayName(), - 'icon' => 'icon-user', - 'subline' => $status['message'] ?? '', - 'value' => [ - 'shareType' => IShare::TYPE_USER, - 'shareWith' => $user->getUID(), - ], - 'shareWithDisplayNameUnique' => $userEmail !== null && $userEmail !== '' ? $userEmail : $uid, - 'status' => $status, - ]; - } + $displayName = $user->getDisplayName(); + if ($uniqueDisplayName !== null) { + $displayName .= ' (' . $uniqueDisplayName . ')'; } + + $status = $userStatuses[$user->getUID()] ?? []; + + $result[$type][] = [ + 'label' => $displayName, + 'subline' => $status['message'] ?? '', + 'icon' => 'icon-user', + 'value' => [ + 'shareType' => IShare::TYPE_USER, + 'shareWith' => $user->getUID(), + ], + 'shareWithDisplayNameUnique' => $uniqueDisplayName ?? $user->getSystemEMailAddress() ?: $user->getUID(), + 'status' => $status, + ]; } $type = new SearchResultType('users'); $searchResult->addResultSet($type, $result['wide'], $result['exact']); - if (count($result['exact'])) { + if ($result['exact'] !== []) { $searchResult->markExactIdMatch($type); } - return $hasMoreResults; - } - - public function takeOutCurrentUser(array &$users): void { - $currentUser = $this->userSession->getUser(); - if (!is_null($currentUser)) { - if (isset($users[$currentUser->getUID()])) { - unset($users[$currentUser->getUID()]); - } - } + return count($users) < $limit; } } diff --git a/tests/lib/Collaboration/Collaborators/UserPluginTest.php b/tests/lib/Collaboration/Collaborators/UserPluginTest.php deleted file mode 100644 index cb4949fb86d0f..0000000000000 --- a/tests/lib/Collaboration/Collaborators/UserPluginTest.php +++ /dev/null @@ -1,810 +0,0 @@ -config = $this->createMock(IConfig::class); - - $this->userManager = $this->createMock(IUserManager::class); - - $this->groupManager = $this->createMock(IGroupManager::class); - - $this->session = $this->createMock(IUserSession::class); - - $this->knownUserService = $this->createMock(KnownUserService::class); - - $this->userStatusManager = $this->createMock(IUserStatusManager::class); - - $this->searchResult = new SearchResult(); - - $this->user = $this->getUserMock('admin', 'Administrator'); - } - - public function instantiatePlugin() { - // cannot be done within setUp, because dependent mocks needs to be set - // up with configuration etc. first - $this->plugin = new UserPlugin( - $this->config, - $this->userManager, - $this->groupManager, - $this->session, - $this->knownUserService, - $this->userStatusManager - ); - } - - public function mockConfig($mockedSettings) { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) use ($mockedSettings) { - return $mockedSettings[$appName][$key] ?? $default; - } - ); - } - - public function getUserMock($uid, $displayName, $enabled = true, $groups = []) { - $user = $this->createMock(IUser::class); - - $user->expects($this->any()) - ->method('getUID') - ->willReturn($uid); - - $user->expects($this->any()) - ->method('getDisplayName') - ->willReturn($displayName); - - $user->expects($this->any()) - ->method('isEnabled') - ->willReturn($enabled); - - return $user; - } - - public function getGroupMock($gid) { - $group = $this->createMock(IGroup::class); - - $group->expects($this->any()) - ->method('getGID') - ->willReturn($gid); - - return $group; - } - - public function dataGetUsers(): array { - return [ - ['test', false, true, [], [], [], [], true, false], - ['test', false, false, [], [], [], [], true, false], - ['test', true, true, [], [], [], [], true, false], - ['test', true, false, [], [], [], [], true, false], - [ - 'test', false, true, [], [], - [ - ['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test'], - ], [], true, $this->getUserMock('test', 'Test'), - ], - [ - 'test', false, false, [], [], - [ - ['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test'], - ], [], true, $this->getUserMock('test', 'Test'), - ], - [ - 'test', true, true, [], [], - [], [], true, $this->getUserMock('test', 'Test'), - ], - [ - 'test', true, false, [], [], - [], [], true, $this->getUserMock('test', 'Test'), - ], - [ - 'test', true, true, ['test-group'], [['test-group', 'test', 2, 0, []]], - [ - ['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test'], - ], [], true, $this->getUserMock('test', 'Test'), - ], - [ - 'test', true, false, ['test-group'], [['test-group', 'test', 2, 0, []]], - [ - ['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test'], - ], [], true, $this->getUserMock('test', 'Test'), - ], - [ - 'test', - false, - true, - [], - [ - $this->getUserMock('test1', 'Test One'), - ], - [], - [ - ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test1'], - ], - true, - false, - ], - [ - 'test', - false, - false, - [], - [ - $this->getUserMock('test1', 'Test One'), - ], - [], - [], - true, - false, - ], - [ - 'test', - false, - true, - [], - [ - $this->getUserMock('test1', 'Test One'), - $this->getUserMock('test2', 'Test Two'), - ], - [], - [ - ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test1'], - ['label' => 'Test Two', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test2'], - ], - false, - false, - ], - [ - 'test', - false, - false, - [], - [ - $this->getUserMock('test1', 'Test One'), - $this->getUserMock('test2', 'Test Two'), - ], - [], - [], - true, - false, - ], - [ - 'test', - false, - true, - [], - [ - $this->getUserMock('test0', 'Test'), - $this->getUserMock('test1', 'Test One'), - $this->getUserMock('test2', 'Test Two'), - ], - [ - ['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test0'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test0'], - ], - [ - ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test1'], - ['label' => 'Test Two', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test2'], - ], - false, - false, - ], - [ - 'test', - false, - true, - [], - [ - $this->getUserMock('test0', 'Test'), - $this->getUserMock('test1', 'Test One'), - $this->getUserMock('test2', 'Test Two'), - ], - [ - ['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test0'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test0'], - ], - [ - ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test1'], - ['label' => 'Test Two', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test2'], - ], - false, - false, - [], - true, - ], - [ - 'test', - false, - false, - [], - [ - $this->getUserMock('test0', 'Test'), - $this->getUserMock('test1', 'Test One'), - $this->getUserMock('test2', 'Test Two'), - ], - [ - ['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test0'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test0'], - ], - [], - true, - false, - ], - [ - 'test', - true, - true, - ['abc', 'xyz'], - [ - ['abc', 'test', 2, 0, ['test1' => 'Test One']], - ['xyz', 'test', 2, 0, []], - ], - [], - [ - ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test1'], - ], - true, - false, - [['test1', $this->getUserMock('test1', 'Test One')]], - ], - [ - 'test', - true, - false, - ['abc', 'xyz'], - [ - ['abc', 'test', 2, 0, ['test1' => 'Test One']], - ['xyz', 'test', 2, 0, []], - ], - [], - [], - true, - false, - [['test1', $this->getUserMock('test1', 'Test One')]], - ], - [ - 'test', - true, - true, - ['abc', 'xyz'], - [ - ['abc', 'test', 2, 0, [ - 'test1' => 'Test One', - 'test2' => 'Test Two', - ]], - ['xyz', 'test', 2, 0, [ - 'test1' => 'Test One', - 'test2' => 'Test Two', - ]], - ], - [], - [ - ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test1'], - ['label' => 'Test Two', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test2'], - ], - true, - false, - [ - ['test1', $this->getUserMock('test1', 'Test One')], - ['test2', $this->getUserMock('test2', 'Test Two')], - ], - ], - [ - 'test', - true, - false, - ['abc', 'xyz'], - [ - ['abc', 'test', 2, 0, [ - 'test1' => 'Test One', - 'test2' => 'Test Two', - ]], - ['xyz', 'test', 2, 0, [ - 'test1' => 'Test One', - 'test2' => 'Test Two', - ]], - ], - [], - [], - true, - false, - [ - ['test1', $this->getUserMock('test1', 'Test One')], - ['test2', $this->getUserMock('test2', 'Test Two')], - ], - ], - [ - 'test', - true, - true, - ['abc', 'xyz'], - [ - ['abc', 'test', 2, 0, [ - 'test' => 'Test One', - ]], - ['xyz', 'test', 2, 0, [ - 'test2' => 'Test Two', - ]], - ], - [ - ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test'], - ], - [ - ['label' => 'Test Two', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test2'], - ], - false, - false, - [ - ['test', $this->getUserMock('test', 'Test One')], - ['test2', $this->getUserMock('test2', 'Test Two')], - ], - ], - [ - 'test', - true, - false, - ['abc', 'xyz'], - [ - ['abc', 'test', 2, 0, [ - 'test' => 'Test One', - ]], - ['xyz', 'test', 2, 0, [ - 'test2' => 'Test Two', - ]], - ], - [ - ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test'], - ], - [], - true, - false, - [ - ['test', $this->getUserMock('test', 'Test One')], - ['test2', $this->getUserMock('test2', 'Test Two')], - ], - ], - ]; - } - - /** - * - * @param string $searchTerm - * @param bool $shareWithGroupOnly - * @param bool $shareeEnumeration - * @param array $groupResponse - * @param array $userResponse - * @param array $exactExpected - * @param array $expected - * @param bool $reachedEnd - * @param bool|IUser $singleUser - * @param array $users - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetUsers')] - public function testSearch( - $searchTerm, - $shareWithGroupOnly, - $shareeEnumeration, - array $groupResponse, - array $userResponse, - array $exactExpected, - array $expected, - $reachedEnd, - $singleUser, - array $users = [], - $shareeEnumerationPhone = false, - ): void { - $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()) - ->method('getUser') - ->willReturn($this->user); - - if (!$shareWithGroupOnly) { - if ($shareeEnumerationPhone) { - $this->userManager->expects($this->once()) - ->method('searchKnownUsersByDisplayName') - ->with($this->user->getUID(), $searchTerm, $this->limit, $this->offset) - ->willReturn($userResponse); - - $this->knownUserService->method('isKnownToUser') - ->willReturnMap([ - [$this->user->getUID(), 'test0', true], - [$this->user->getUID(), 'test1', true], - [$this->user->getUID(), 'test2', true], - ]); - } else { - $this->userManager->expects($this->once()) - ->method('searchDisplayName') - ->with($searchTerm, $this->limit, $this->offset) - ->willReturn($userResponse); - } - } else { - $this->groupManager->method('getUserGroupIds') - ->with($this->user) - ->willReturn($groupResponse); - - if ($singleUser !== false) { - $this->groupManager->method('getUserGroupIds') - ->with($singleUser) - ->willReturn($groupResponse); - } - - $this->groupManager->method('displayNamesInGroup') - ->willReturnMap($userResponse); - } - - if ($singleUser !== false) { - $users[] = [$searchTerm, $singleUser]; - } - - if (!empty($users)) { - $this->userManager->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap($users); - } - - $moreResults = $this->plugin->search($searchTerm, $this->limit, $this->offset, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertEquals($exactExpected, $result['exact']['users']); - $this->assertEquals($expected, $result['users']); - $this->assertSame($reachedEnd, $moreResults); - } - - public static function takeOutCurrentUserProvider(): array { - $inputUsers = [ - 'alice' => 'Alice', - 'bob' => 'Bob', - 'carol' => 'Carol', - ]; - return [ - [ - $inputUsers, - ['alice', 'carol'], - 'bob', - ], - [ - $inputUsers, - ['alice', 'bob', 'carol'], - 'dave', - ], - [ - $inputUsers, - ['alice', 'bob', 'carol'], - null, - ], - ]; - } - - /** - * @param array $users - * @param array $expectedUIDs - * @param $currentUserId - */ - #[\PHPUnit\Framework\Attributes\DataProvider('takeOutCurrentUserProvider')] - public function testTakeOutCurrentUser(array $users, array $expectedUIDs, $currentUserId): void { - $this->instantiatePlugin(); - - $this->session->expects($this->once()) - ->method('getUser') - ->willReturnCallback(function () use ($currentUserId) { - if ($currentUserId !== null) { - return $this->getUserMock($currentUserId, $currentUserId); - } - return null; - }); - - $this->plugin->takeOutCurrentUser($users); - $this->assertSame($expectedUIDs, array_keys($users)); - } - - public static function dataSearchEnumeration(): array { - return [ - [ - 'test', - ['groupA'], - [ - ['uid' => 'test1', 'groups' => ['groupA']], - ['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_dn' => 'yes', - ], - ] - ], - [ - 'test1', - ['groupA'], - [ - ['uid' => 'test1', 'groups' => ['groupA']], - ['uid' => 'test2', 'groups' => ['groupB']], - ], - ['exact' => ['test1'], 'wide' => []], - ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], - ], - [ - 'test', - ['groupA'], - [ - ['uid' => 'test1', 'groups' => ['groupA']], - ['uid' => 'test2', 'groups' => ['groupB', 'groupA']], - ], - ['exact' => [], 'wide' => ['test1', 'test2']], - ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], - ], - [ - 'test', - ['groupA'], - [ - ['uid' => 'test1', 'groups' => ['groupA', 'groupC']], - ['uid' => 'test2', 'groups' => ['groupB', 'groupA']], - ], - ['exact' => [], 'wide' => ['test1', 'test2']], - ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], - ], - [ - 'test', - ['groupC', 'groupB'], - [ - ['uid' => 'test1', 'groups' => ['groupA', 'groupC']], - ['uid' => 'test2', 'groups' => ['groupB', 'groupA']], - ], - ['exact' => [], 'wide' => ['test1', 'test2']], - ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], - ], - [ - 'test', - [], - [ - ['uid' => 'test1', 'groups' => ['groupA']], - ['uid' => 'test2', 'groups' => ['groupB', 'groupA']], - ], - ['exact' => [], 'wide' => []], - ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], - ], - [ - 'test', - ['groupC', 'groupB'], - [ - ['uid' => 'test1', 'groups' => []], - ['uid' => 'test2', 'groups' => []], - ], - ['exact' => [], 'wide' => []], - ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], - ], - [ - 'test', - ['groupC', 'groupB'], - [ - ['uid' => 'test1', 'groups' => []], - ['uid' => 'test2', 'groups' => []], - ], - ['exact' => [], 'wide' => []], - ['core' => ['shareapi_restrict_user_enumeration_to_group' => 'yes']], - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSearchEnumeration')] - public function testSearchEnumerationLimit($search, $userGroups, $matchingUsers, $result, $mockedSettings): void { - $this->mockConfig($mockedSettings); - - $userResults = []; - foreach ($matchingUsers as $user) { - $userResults[$user['uid']] = $user['uid']; - } - - $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, - ]; - }, $result['wide']); - - $this->userManager - ->method('get') - ->willReturnCallback(function ($userId) use ($userResults) { - if (isset($userResults[$userId])) { - return $this->getUserMock($userId, $userId); - } - return null; - }); - $this->userManager - ->method('searchDisplayName') - ->willReturnCallback(function ($search) use ($matchingUsers) { - $users = array_filter( - $matchingUsers, - fn ($user) => str_contains(strtolower($user['displayName']), strtolower($search)) - ); - return array_map( - fn ($user) => $this->getUserMock($user['uid'], $user['displayName']), - $users); - }); - - $this->groupManager->method('displayNamesInGroup') - ->willReturn($userResults); - - - $this->session->expects($this->any()) - ->method('getUser') - ->willReturn($this->getUserMock('test', 'foo')); - $this->groupManager->expects($this->any()) - ->method('getUserGroupIds') - ->willReturnCallback(function ($user) use ($matchingUsers, $userGroups) { - static $firstCall = true; - if ($firstCall) { - $firstCall = false; - // current user - return $userGroups; - } - $neededObject = array_filter( - $matchingUsers, - function ($e) use ($user) { - return $user->getUID() === $e['uid']; - } - ); - if (count($neededObject) > 0) { - return array_shift($neededObject)['groups']; - } - return []; - }); - - $this->instantiatePlugin(); - $this->plugin->search($search, $this->limit, $this->offset, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertEquals($mappedResultExact, $result['exact']['users']); - $this->assertEquals($mappedResultWide, $result['users']); - } -}