Merge pull request #57511 from nextcloud/fix/userplugin/rewrite

This commit is contained in:
Kate 2026-01-21 15:10:11 +01:00 committed by GitHub
commit 677d42555e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 623 additions and 1012 deletions

View file

@ -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"

View file

@ -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');
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -4,277 +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 $shareeEnumerationfullMatchDisplayname;
protected bool $shareeEnumerationFullMatchEmail;
protected bool $shareeEnumerationFullMatchIgnoreSecondDisplayName;
readonly class UserPlugin implements ISearchPlugin {
public function __construct(
private IConfig $config,
private IAppConfig $appConfig,
private IUserManager $userManager,
private IGroupManager $groupManager,
private IUserSession $userSession,
private KnownUserService $knownUserService,
private IUserStatusManager $userStatusManager,
private mixed $shareWithGroupOnlyExcludeGroupsList = [],
private 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_user_id', 'yes') === 'yes';
$this->shareeEnumerationfullMatchDisplayname = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_displayname', '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<string, array{0: 'wide'|'exact', 1: IUser}> $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 && $this->shareeEnumerationfullMatchDisplayname) {
$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);
// 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';
if (!$this->shareeEnumeration || count($users) < $limit) {
$hasMoreResults = true;
}
$lowerSearch = mb_strtolower($search);
$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;
$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 ($shareeEnumerationFullMatchUserId) {
$user = $this->userManager->get($search);
if ($user !== null) {
$users[$user->getUID()] = ['exact', $user];
}
}
if (
$this->shareeEnumerationFullMatch
&& $lowerSearch !== ''
&& (
strtolower($uid) === $lowerSearch
|| ($this->shareeEnumerationfullMatchDisplayname && 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 ($this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $user->getUID())) {
$addToWideResults = true;
}
if (!$addToWideResults && $this->shareeEnumerationInGroupOnly) {
$commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user));
if (!empty($commonGroups)) {
$addToWideResults = true;
}
}
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 ($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 ($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;
uasort($users, static fn (array $a, array $b): int => strcasecmp($a[1]->getDisplayName(), $b[1]->getDisplayName()));
if ($this->shareWithGroupOnly) {
// Only add, if we have a common group
$commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user));
$addUser = !empty($commonGroups);
}
if (isset($users[$currentUser->getUID()])) {
unset($users[$currentUser->getUID()]);
}
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,
];
}
$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])) !== []);
}
$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,
];
}
$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)),
);
$result = ['wide' => [], 'exact' => []];
foreach ($users as $match) {
$match[2] ??= null;
[$type, $user, $uniqueDisplayName] = $match;
$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;
}
}

View file

@ -1,781 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Collaboration\Collaborators;
use OC\Collaboration\Collaborators\SearchResult;
use OC\Collaboration\Collaborators\UserPlugin;
use OC\KnownUser\KnownUserService;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IShare;
use OCP\UserStatus\IManager as IUserStatusManager;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class UserPluginTest extends TestCase {
private IConfig&MockObject $config;
private IUserManager&MockObject $userManager;
private IGroupManager&MockObject $groupManager;
private IUserSession&MockObject $session;
private KnownUserService&MockObject $knownUserService;
private IUserStatusManager&MockObject $userStatusManager;
private IUser&MockObject $user;
/** @var UserPlugin */
protected $plugin;
/** @var ISearchResult */
protected $searchResult;
protected int $limit = 2;
protected int $offset = 0;
protected function setUp(): void {
parent::setUp();
$this->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(): void {
// 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): void {
$this->config->expects($this->any())
->method('getAppValue')
->willReturnCallback(
function ($appName, $key, $default) use ($mockedSettings) {
return $mockedSettings[$appName][$key] ?? $default;
}
);
}
public function getUserMock(string $uid, string $displayName, bool $enabled = true, array $groups = []): IUser&MockObject {
$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 static 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, ['test', 'Test'],
],
[
'test', false, false, [], [],
[
['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test'],
], [], true, ['test', 'Test'],
],
[
'test', true, true, [], [],
[], [], true, ['test', 'Test'],
],
[
'test', true, false, [], [],
[], [], true, ['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, ['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, ['test', 'Test'],
],
[
'test',
false,
true,
[],
[
['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,
[],
[
['test1', 'Test One'],
],
[],
[],
true,
false,
],
[
'test',
false,
true,
[],
[
['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'],
],
false,
false,
],
[
'test',
false,
false,
[],
[
['test1', 'Test One'],
['test2', 'Test Two'],
],
[],
[],
true,
false,
],
[
'test',
false,
true,
[],
[
['test0', 'Test'],
['test1', 'Test One'],
['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,
[],
[
['test0', 'Test'],
['test1', 'Test One'],
['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,
[],
[
['test0', 'Test'],
['test1', 'Test One'],
['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', ['test1', 'Test One']]],
],
[
'test',
true,
false,
['abc', 'xyz'],
[
['abc', 'test', 2, 0, ['test1' => 'Test One']],
['xyz', 'test', 2, 0, []],
],
[],
[],
true,
false,
[['test1', ['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', ['test1', 'Test One']],
['test2', ['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', ['test1', 'Test One']],
['test2', ['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', ['test', 'Test One']],
['test2', ['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', ['test', 'Test One']],
['test2', ['test2', 'Test Two']],
],
],
];
}
#[DataProvider('dataGetUsers')]
public function testSearch(
string $searchTerm,
bool $shareWithGroupOnly,
bool $shareeEnumeration,
array $groupResponse,
array $userResponse,
array $exactExpected,
array $expected,
bool $reachedEnd,
array|false $singleUser,
array $users = [],
bool $shareeEnumerationPhone = false,
): void {
if ($singleUser !== false) {
$singleUser = $this->getUserMock(...$singleUser);
}
$users = array_map(
fn ($args) => [$args[0], $this->getUserMock(...$args[1])],
$users
);
$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) {
$userResponse = array_map(
fn ($args) => $this->getUserMock(...$args),
$userResponse
);
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],
]);
}
$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,
],
];
}
#[DataProvider('takeOutCurrentUserProvider')]
public function testTakeOutCurrentUser(array $users, array $expectedUIDs, ?string $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_user_id' => '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_user_id' => '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']],
],
];
}
#[DataProvider('dataSearchEnumeration')]
public function testSearchEnumerationLimit(string $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']);
}
}