aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Collaboration/Collaborators
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Collaboration/Collaborators')
-rw-r--r--lib/private/Collaboration/Collaborators/GroupPlugin.php130
-rw-r--r--lib/private/Collaboration/Collaborators/LookupPlugin.php97
-rw-r--r--lib/private/Collaboration/Collaborators/MailPlugin.php262
-rw-r--r--lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php73
-rw-r--r--lib/private/Collaboration/Collaborators/RemotePlugin.php166
-rw-r--r--lib/private/Collaboration/Collaborators/Search.php120
-rw-r--r--lib/private/Collaboration/Collaborators/SearchResult.php89
-rw-r--r--lib/private/Collaboration/Collaborators/UserPlugin.php266
8 files changed, 1203 insertions, 0 deletions
diff --git a/lib/private/Collaboration/Collaborators/GroupPlugin.php b/lib/private/Collaboration/Collaborators/GroupPlugin.php
new file mode 100644
index 00000000000..a59d5981825
--- /dev/null
+++ b/lib/private/Collaboration/Collaborators/GroupPlugin.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Collaboration\Collaborators;
+
+use OCP\Collaboration\Collaborators\ISearchPlugin;
+use OCP\Collaboration\Collaborators\ISearchResult;
+use OCP\Collaboration\Collaborators\SearchResultType;
+use OCP\IConfig;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUserSession;
+use OCP\Share\IShare;
+
+class GroupPlugin implements ISearchPlugin {
+ protected bool $shareeEnumeration;
+
+ protected bool $shareWithGroupOnly;
+
+ protected bool $shareeEnumerationInGroupOnly;
+
+ protected bool $groupSharingDisabled;
+
+ public function __construct(
+ private IConfig $config,
+ private IGroupManager $groupManager,
+ private IUserSession $userSession,
+ private mixed $shareWithGroupOnlyExcludeGroupsList = [],
+ ) {
+ $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
+ $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
+ $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
+ $this->groupSharingDisabled = $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'no';
+
+ 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 {
+ if ($this->groupSharingDisabled) {
+ return false;
+ }
+
+ $hasMoreResults = false;
+ $result = ['wide' => [], 'exact' => []];
+
+ $groups = $this->groupManager->search($search, $limit, $offset);
+ $groupIds = array_map(function (IGroup $group) {
+ return $group->getGID();
+ }, $groups);
+
+ if (!$this->shareeEnumeration || count($groups) < $limit) {
+ $hasMoreResults = true;
+ }
+
+ $userGroups = [];
+ if (!empty($groups) && ($this->shareWithGroupOnly || $this->shareeEnumerationInGroupOnly)) {
+ // Intersect all the groups that match with the groups this user is a member of
+ $userGroups = $this->groupManager->getUserGroups($this->userSession->getUser());
+ $userGroups = array_map(function (IGroup $group) {
+ return $group->getGID();
+ }, $userGroups);
+ $groupIds = array_intersect($groupIds, $userGroups);
+
+ // ShareWithGroupOnly filtering
+ $groupIds = array_diff($groupIds, $this->shareWithGroupOnlyExcludeGroupsList);
+ }
+
+ $lowerSearch = strtolower($search);
+ foreach ($groups as $group) {
+ if ($group->hideFromCollaboration()) {
+ continue;
+ }
+
+ // FIXME: use a more efficient approach
+ $gid = $group->getGID();
+ if (!in_array($gid, $groupIds)) {
+ continue;
+ }
+ if (strtolower($gid) === $lowerSearch || strtolower($group->getDisplayName()) === $lowerSearch) {
+ $result['exact'][] = [
+ 'label' => $group->getDisplayName(),
+ 'value' => [
+ 'shareType' => IShare::TYPE_GROUP,
+ 'shareWith' => $gid,
+ ],
+ ];
+ } else {
+ if ($this->shareeEnumerationInGroupOnly && !in_array($group->getGID(), $userGroups, true)) {
+ continue;
+ }
+ $result['wide'][] = [
+ 'label' => $group->getDisplayName(),
+ 'value' => [
+ 'shareType' => IShare::TYPE_GROUP,
+ 'shareWith' => $gid,
+ ],
+ ];
+ }
+ }
+
+ if ($offset === 0 && empty($result['exact'])) {
+ // 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
+ $group = $this->groupManager->get($search);
+ if ($group instanceof IGroup && !$group->hideFromCollaboration() && (!$this->shareWithGroupOnly || in_array($group->getGID(), $userGroups))) {
+ $result['exact'][] = [
+ 'label' => $group->getDisplayName(),
+ 'value' => [
+ 'shareType' => IShare::TYPE_GROUP,
+ 'shareWith' => $group->getGID(),
+ ],
+ ];
+ }
+ }
+
+ if (!$this->shareeEnumeration) {
+ $result['wide'] = [];
+ }
+
+ $type = new SearchResultType('groups');
+ $searchResult->addResultSet($type, $result['wide'], $result['exact']);
+
+ return $hasMoreResults;
+ }
+}
diff --git a/lib/private/Collaboration/Collaborators/LookupPlugin.php b/lib/private/Collaboration/Collaborators/LookupPlugin.php
new file mode 100644
index 00000000000..fb6b9f2e0e8
--- /dev/null
+++ b/lib/private/Collaboration/Collaborators/LookupPlugin.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Collaboration\Collaborators;
+
+use OCP\Collaboration\Collaborators\ISearchPlugin;
+use OCP\Collaboration\Collaborators\ISearchResult;
+use OCP\Collaboration\Collaborators\SearchResultType;
+use OCP\Federation\ICloudIdManager;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\IUserSession;
+use OCP\Share\IShare;
+use Psr\Log\LoggerInterface;
+
+class LookupPlugin implements ISearchPlugin {
+ /** @var string remote part of the current user's cloud id */
+ private string $currentUserRemote;
+
+ public function __construct(
+ private IConfig $config,
+ private IClientService $clientService,
+ IUserSession $userSession,
+ private ICloudIdManager $cloudIdManager,
+ private LoggerInterface $logger,
+ ) {
+ $currentUserCloudId = $userSession->getUser()->getCloudId();
+ $this->currentUserRemote = $cloudIdManager->resolveCloudId($currentUserCloudId)->getRemote();
+ }
+
+ public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
+ $isGlobalScaleEnabled = $this->config->getSystemValueBool('gs.enabled', false);
+ $isLookupServerEnabled = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no') === 'yes';
+ $hasInternetConnection = $this->config->getSystemValueBool('has_internet_connection', true);
+
+ // If case of Global Scale we always search the lookup server
+ // TODO: Reconsider using the lookup server for non-global scale
+ // if (!$isGlobalScaleEnabled && (!$isLookupServerEnabled || !$hasInternetConnection || $disableLookupServer)) {
+ if (!$isGlobalScaleEnabled) {
+ return false;
+ }
+
+ $lookupServerUrl = $this->config->getSystemValueString('lookup_server', 'https://lookup.nextcloud.com');
+ if (empty($lookupServerUrl)) {
+ return false;
+ }
+ $lookupServerUrl = rtrim($lookupServerUrl, '/');
+ $result = [];
+
+ try {
+ $client = $this->clientService->newClient();
+ $response = $client->get(
+ $lookupServerUrl . '/users?search=' . urlencode($search),
+ [
+ 'timeout' => 10,
+ 'connect_timeout' => 3,
+ ]
+ );
+
+ $body = json_decode($response->getBody(), true);
+
+ foreach ($body as $lookup) {
+ try {
+ $remote = $this->cloudIdManager->resolveCloudId($lookup['federationId'])->getRemote();
+ } catch (\Exception $e) {
+ $this->logger->error('Can not parse federated cloud ID "' . $lookup['federationId'] . '"', [
+ 'exception' => $e,
+ ]);
+ continue;
+ }
+ if ($this->currentUserRemote === $remote) {
+ continue;
+ }
+ $name = $lookup['name']['value'] ?? '';
+ $label = empty($name) ? $lookup['federationId'] : $name . ' (' . $lookup['federationId'] . ')';
+ $result[] = [
+ 'label' => $label,
+ 'value' => [
+ 'shareType' => IShare::TYPE_REMOTE,
+ 'globalScale' => $isGlobalScaleEnabled,
+ 'shareWith' => $lookup['federationId'],
+ ],
+ 'extra' => $lookup,
+ ];
+ }
+ } catch (\Exception $e) {
+ }
+
+ $type = new SearchResultType('lookup');
+ $searchResult->addResultSet($type, $result, []);
+
+ return false;
+ }
+}
diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php
new file mode 100644
index 00000000000..55e3945ace2
--- /dev/null
+++ b/lib/private/Collaboration/Collaborators/MailPlugin.php
@@ -0,0 +1,262 @@
+<?php
+
+/**
+ * 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\Contacts\IManager;
+use OCP\Federation\ICloudId;
+use OCP\Federation\ICloudIdManager;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Mail\IMailer;
+use OCP\Share\IShare;
+
+class MailPlugin implements ISearchPlugin {
+ protected bool $shareWithGroupOnly;
+
+ protected bool $shareeEnumeration;
+
+ protected bool $shareeEnumerationInGroupOnly;
+
+ protected bool $shareeEnumerationPhone;
+
+ protected bool $shareeEnumerationFullMatch;
+
+ protected bool $shareeEnumerationFullMatchEmail;
+
+ public function __construct(
+ private IManager $contactsManager,
+ private ICloudIdManager $cloudIdManager,
+ private IConfig $config,
+ private IGroupManager $groupManager,
+ private KnownUserService $knownUserService,
+ private IUserSession $userSession,
+ private IMailer $mailer,
+ private mixed $shareWithGroupOnlyExcludeGroupsList = [],
+ ) {
+ $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
+ $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === '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->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';
+
+ if ($this->shareWithGroupOnly) {
+ $this->shareWithGroupOnlyExcludeGroupsList = json_decode($this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''), true) ?? [];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
+ if ($this->shareeEnumerationFullMatch && !$this->shareeEnumerationFullMatchEmail) {
+ return false;
+ }
+
+ // Extract the email address from "Foo Bar <foo.bar@example.tld>" and then search with "foo.bar@example.tld" instead
+ $result = preg_match('/<([^@]+@.+)>$/', $search, $matches);
+ if ($result && filter_var($matches[1], FILTER_VALIDATE_EMAIL)) {
+ return $this->search($matches[1], $limit, $offset, $searchResult);
+ }
+
+ $currentUserId = $this->userSession->getUser()->getUID();
+
+ $result = $userResults = ['wide' => [], 'exact' => []];
+ $userType = new SearchResultType('users');
+ $emailType = new SearchResultType('emails');
+
+ // Search in contacts
+ $addressBookContacts = $this->contactsManager->search(
+ $search,
+ ['EMAIL', 'FN'],
+ [
+ 'limit' => $limit,
+ 'offset' => $offset,
+ 'enumeration' => $this->shareeEnumeration,
+ 'fullmatch' => $this->shareeEnumerationFullMatch,
+ ]
+ );
+ $lowerSearch = strtolower($search);
+ foreach ($addressBookContacts as $contact) {
+ if (isset($contact['EMAIL'])) {
+ $emailAddresses = $contact['EMAIL'];
+ if (\is_string($emailAddresses)) {
+ $emailAddresses = [$emailAddresses];
+ }
+ foreach ($emailAddresses as $type => $emailAddress) {
+ $displayName = $emailAddress;
+ $emailAddressType = null;
+ if (\is_array($emailAddress)) {
+ $emailAddressData = $emailAddress;
+ $emailAddress = $emailAddressData['value'];
+ $emailAddressType = $emailAddressData['type'];
+ }
+
+ if (!filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) {
+ continue;
+ }
+
+ if (isset($contact['FN'])) {
+ $displayName = $contact['FN'] . ' (' . $emailAddress . ')';
+ }
+ $exactEmailMatch = strtolower($emailAddress) === $lowerSearch;
+
+ if (isset($contact['isLocalSystemBook'])) {
+ if ($this->shareWithGroupOnly) {
+ /*
+ * Check if the user may share with the user associated with the e-mail of the just found contact
+ */
+ $userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
+
+ // ShareWithGroupOnly filtering
+ $userGroups = array_diff($userGroups, $this->shareWithGroupOnlyExcludeGroupsList);
+
+ $found = false;
+ foreach ($userGroups as $userGroup) {
+ if ($this->groupManager->isInGroup($contact['UID'], $userGroup)) {
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ continue;
+ }
+ }
+ if ($exactEmailMatch && $this->shareeEnumerationFullMatch) {
+ try {
+ $cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0] ?? '');
+ } catch (\InvalidArgumentException $e) {
+ continue;
+ }
+
+ if (!$this->isCurrentUser($cloud) && !$searchResult->hasResult($userType, $cloud->getUser())) {
+ $singleResult = [[
+ 'label' => $displayName,
+ 'uuid' => $contact['UID'] ?? $emailAddress,
+ 'name' => $contact['FN'] ?? $displayName,
+ 'value' => [
+ 'shareType' => IShare::TYPE_USER,
+ 'shareWith' => $cloud->getUser(),
+ ],
+ 'shareWithDisplayNameUnique' => !empty($emailAddress) ? $emailAddress : $cloud->getUser()
+
+ ]];
+ $searchResult->addResultSet($userType, [], $singleResult);
+ $searchResult->markExactIdMatch($emailType);
+ }
+ return false;
+ }
+
+ if ($this->shareeEnumeration) {
+ try {
+ $cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0] ?? '');
+ } catch (\InvalidArgumentException $e) {
+ continue;
+ }
+
+ $addToWide = !($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone);
+ if (!$addToWide && $this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $contact['UID'])) {
+ $addToWide = true;
+ }
+
+ if (!$addToWide && $this->shareeEnumerationInGroupOnly) {
+ $addToWide = false;
+ $userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
+ foreach ($userGroups as $userGroup) {
+ if ($this->groupManager->isInGroup($contact['UID'], $userGroup)) {
+ $addToWide = true;
+ break;
+ }
+ }
+ }
+ if ($addToWide && !$this->isCurrentUser($cloud) && !$searchResult->hasResult($userType, $cloud->getUser())) {
+ $userResults['wide'][] = [
+ 'label' => $displayName,
+ 'uuid' => $contact['UID'] ?? $emailAddress,
+ 'name' => $contact['FN'] ?? $displayName,
+ 'value' => [
+ 'shareType' => IShare::TYPE_USER,
+ 'shareWith' => $cloud->getUser(),
+ ],
+ 'shareWithDisplayNameUnique' => !empty($emailAddress) ? $emailAddress : $cloud->getUser()
+ ];
+ continue;
+ }
+ }
+ continue;
+ }
+
+ if ($exactEmailMatch
+ || (isset($contact['FN']) && strtolower($contact['FN']) === $lowerSearch)) {
+ if ($exactEmailMatch) {
+ $searchResult->markExactIdMatch($emailType);
+ }
+ $result['exact'][] = [
+ 'label' => $displayName,
+ 'uuid' => $contact['UID'] ?? $emailAddress,
+ 'name' => $contact['FN'] ?? $displayName,
+ 'type' => $emailAddressType ?? '',
+ 'value' => [
+ 'shareType' => IShare::TYPE_EMAIL,
+ 'shareWith' => $emailAddress,
+ ],
+ ];
+ } else {
+ $result['wide'][] = [
+ 'label' => $displayName,
+ 'uuid' => $contact['UID'] ?? $emailAddress,
+ 'name' => $contact['FN'] ?? $displayName,
+ 'type' => $emailAddressType ?? '',
+ 'value' => [
+ 'shareType' => IShare::TYPE_EMAIL,
+ 'shareWith' => $emailAddress,
+ ],
+ ];
+ }
+ }
+ }
+ }
+
+ $reachedEnd = true;
+ if ($this->shareeEnumeration) {
+ $reachedEnd = (count($result['wide']) < $offset + $limit)
+ && (count($userResults['wide']) < $offset + $limit);
+
+ $result['wide'] = array_slice($result['wide'], $offset, $limit);
+ $userResults['wide'] = array_slice($userResults['wide'], $offset, $limit);
+ }
+
+ if (!$searchResult->hasExactIdMatch($emailType) && $this->mailer->validateMailAddress($search)) {
+ $result['exact'][] = [
+ 'label' => $search,
+ 'uuid' => $search,
+ 'value' => [
+ 'shareType' => IShare::TYPE_EMAIL,
+ 'shareWith' => $search,
+ ],
+ ];
+ }
+
+ if (!empty($userResults['wide'])) {
+ $searchResult->addResultSet($userType, $userResults['wide'], []);
+ }
+ $searchResult->addResultSet($emailType, $result['wide'], $result['exact']);
+
+ return !$reachedEnd;
+ }
+
+ public function isCurrentUser(ICloudId $cloud): bool {
+ $currentUser = $this->userSession->getUser();
+ return $currentUser instanceof IUser && $currentUser->getUID() === $cloud->getUser();
+ }
+}
diff --git a/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php b/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php
new file mode 100644
index 00000000000..f4c1793ea0a
--- /dev/null
+++ b/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Collaboration\Collaborators;
+
+use OCP\Collaboration\Collaborators\ISearchPlugin;
+use OCP\Collaboration\Collaborators\ISearchResult;
+use OCP\Collaboration\Collaborators\SearchResultType;
+use OCP\Federation\ICloudFederationProviderManager;
+use OCP\Federation\ICloudIdManager;
+use OCP\Share;
+use OCP\Share\IShare;
+
+class RemoteGroupPlugin implements ISearchPlugin {
+ private bool $enabled = false;
+
+ public function __construct(
+ ICloudFederationProviderManager $cloudFederationProviderManager,
+ private ICloudIdManager $cloudIdManager,
+ ) {
+ try {
+ $fileSharingProvider = $cloudFederationProviderManager->getCloudFederationProvider('file');
+ $supportedShareTypes = $fileSharingProvider->getSupportedShareTypes();
+ if (in_array('group', $supportedShareTypes)) {
+ $this->enabled = true;
+ }
+ } catch (\Exception $e) {
+ // do nothing, just don't enable federated group shares
+ }
+ }
+
+ public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
+ $result = ['wide' => [], 'exact' => []];
+ $resultType = new SearchResultType('remote_groups');
+
+ if ($this->enabled && $this->cloudIdManager->isValidCloudId($search) && $offset === 0) {
+ [$remoteGroup, $serverUrl] = $this->splitGroupRemote($search);
+ $result['exact'][] = [
+ 'label' => $remoteGroup . " ($serverUrl)",
+ 'guid' => $remoteGroup,
+ 'name' => $remoteGroup,
+ 'value' => [
+ 'shareType' => IShare::TYPE_REMOTE_GROUP,
+ 'shareWith' => $search,
+ 'server' => $serverUrl,
+ ],
+ ];
+ }
+
+ $searchResult->addResultSet($resultType, $result['wide'], $result['exact']);
+
+ return true;
+ }
+
+ /**
+ * split group and remote from federated cloud id
+ *
+ * @param string $address federated share address
+ * @return array [user, remoteURL]
+ * @throws \InvalidArgumentException
+ */
+ public function splitGroupRemote($address): array {
+ try {
+ $cloudId = $this->cloudIdManager->resolveCloudId($address);
+ return [$cloudId->getUser(), $cloudId->getRemote()];
+ } catch (\InvalidArgumentException $e) {
+ throw new \InvalidArgumentException('Invalid Federated Cloud ID', 0, $e);
+ }
+ }
+}
diff --git a/lib/private/Collaboration/Collaborators/RemotePlugin.php b/lib/private/Collaboration/Collaborators/RemotePlugin.php
new file mode 100644
index 00000000000..037c6f6cbea
--- /dev/null
+++ b/lib/private/Collaboration/Collaborators/RemotePlugin.php
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Collaboration\Collaborators;
+
+use OCP\Collaboration\Collaborators\ISearchPlugin;
+use OCP\Collaboration\Collaborators\ISearchResult;
+use OCP\Collaboration\Collaborators\SearchResultType;
+use OCP\Contacts\IManager;
+use OCP\Federation\ICloudIdManager;
+use OCP\IConfig;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Share\IShare;
+
+class RemotePlugin implements ISearchPlugin {
+ protected bool $shareeEnumeration;
+
+ private string $userId;
+
+ public function __construct(
+ private IManager $contactsManager,
+ private ICloudIdManager $cloudIdManager,
+ private IConfig $config,
+ private IUserManager $userManager,
+ IUserSession $userSession,
+ ) {
+ $this->userId = $userSession->getUser()?->getUID() ?? '';
+ $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
+ }
+
+ public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
+ $result = ['wide' => [], 'exact' => []];
+ $resultType = new SearchResultType('remotes');
+
+ // Search in contacts
+ $addressBookContacts = $this->contactsManager->search($search, ['CLOUD', 'FN'], [
+ 'limit' => $limit,
+ 'offset' => $offset,
+ 'enumeration' => false,
+ 'fullmatch' => false,
+ ]);
+ foreach ($addressBookContacts as $contact) {
+ if (isset($contact['isLocalSystemBook'])) {
+ continue;
+ }
+ if (isset($contact['CLOUD'])) {
+ $cloudIds = $contact['CLOUD'];
+ if (is_string($cloudIds)) {
+ $cloudIds = [$cloudIds];
+ }
+ $lowerSearch = strtolower($search);
+ foreach ($cloudIds as $cloudId) {
+ $cloudIdType = '';
+ if (\is_array($cloudId)) {
+ $cloudIdData = $cloudId;
+ $cloudId = $cloudIdData['value'];
+ $cloudIdType = $cloudIdData['type'];
+ }
+ try {
+ [$remoteUser, $serverUrl] = $this->splitUserRemote($cloudId);
+ } catch (\InvalidArgumentException $e) {
+ continue;
+ }
+
+ $localUser = $this->userManager->get($remoteUser);
+ /**
+ * Add local share if remote cloud id matches a local user ones
+ */
+ if ($localUser !== null && $remoteUser !== $this->userId && $cloudId === $localUser->getCloudId()) {
+ $result['wide'][] = [
+ 'label' => $contact['FN'],
+ 'uuid' => $contact['UID'],
+ 'value' => [
+ 'shareType' => IShare::TYPE_USER,
+ 'shareWith' => $remoteUser
+ ],
+ 'shareWithDisplayNameUnique' => $contact['EMAIL'] !== null && $contact['EMAIL'] !== '' ? $contact['EMAIL'] : $contact['UID'],
+ ];
+ }
+
+ if (strtolower($contact['FN']) === $lowerSearch || strtolower($cloudId) === $lowerSearch) {
+ if (strtolower($cloudId) === $lowerSearch) {
+ $searchResult->markExactIdMatch($resultType);
+ }
+ $result['exact'][] = [
+ 'label' => $contact['FN'] . " ($cloudId)",
+ 'uuid' => $contact['UID'],
+ 'name' => $contact['FN'],
+ 'type' => $cloudIdType,
+ 'value' => [
+ 'shareType' => IShare::TYPE_REMOTE,
+ 'shareWith' => $cloudId,
+ 'server' => $serverUrl,
+ ],
+ ];
+ } else {
+ $result['wide'][] = [
+ 'label' => $contact['FN'] . " ($cloudId)",
+ 'uuid' => $contact['UID'],
+ 'name' => $contact['FN'],
+ 'type' => $cloudIdType,
+ 'value' => [
+ 'shareType' => IShare::TYPE_REMOTE,
+ 'shareWith' => $cloudId,
+ 'server' => $serverUrl,
+ ],
+ ];
+ }
+ }
+ }
+ }
+
+ if (!$this->shareeEnumeration) {
+ $result['wide'] = [];
+ } else {
+ $result['wide'] = array_slice($result['wide'], $offset, $limit);
+ }
+
+ /**
+ * Add generic share with remote item for valid cloud ids that are not users of the local instance
+ */
+ if (!$searchResult->hasExactIdMatch($resultType) && $this->cloudIdManager->isValidCloudId($search) && $offset === 0) {
+ try {
+ [$remoteUser, $serverUrl] = $this->splitUserRemote($search);
+ $localUser = $this->userManager->get($remoteUser);
+ if ($localUser === null || $search !== $localUser->getCloudId()) {
+ $result['exact'][] = [
+ 'label' => $remoteUser . " ($serverUrl)",
+ 'uuid' => $remoteUser,
+ 'name' => $remoteUser,
+ 'value' => [
+ 'shareType' => IShare::TYPE_REMOTE,
+ 'shareWith' => $search,
+ 'server' => $serverUrl,
+ ],
+ ];
+ }
+ } catch (\InvalidArgumentException $e) {
+ }
+ }
+
+ $searchResult->addResultSet($resultType, $result['wide'], $result['exact']);
+
+ return true;
+ }
+
+ /**
+ * split user and remote from federated cloud id
+ *
+ * @param string $address federated share address
+ * @return array [user, remoteURL]
+ * @throws \InvalidArgumentException
+ */
+ public function splitUserRemote(string $address): array {
+ try {
+ $cloudId = $this->cloudIdManager->resolveCloudId($address);
+ return [$cloudId->getUser(), $this->cloudIdManager->removeProtocolFromUrl($cloudId->getRemote(), true)];
+ } catch (\InvalidArgumentException $e) {
+ throw new \InvalidArgumentException('Invalid Federated Cloud ID', 0, $e);
+ }
+ }
+}
diff --git a/lib/private/Collaboration/Collaborators/Search.php b/lib/private/Collaboration/Collaborators/Search.php
new file mode 100644
index 00000000000..ea39f885fc6
--- /dev/null
+++ b/lib/private/Collaboration/Collaborators/Search.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Collaboration\Collaborators;
+
+use OCP\Collaboration\Collaborators\ISearch;
+use OCP\Collaboration\Collaborators\ISearchPlugin;
+use OCP\Collaboration\Collaborators\ISearchResult;
+use OCP\Collaboration\Collaborators\SearchResultType;
+use OCP\IContainer;
+use OCP\Share\IShare;
+
+class Search implements ISearch {
+ protected array $pluginList = [];
+
+ public function __construct(
+ private IContainer $container,
+ ) {
+ }
+
+ /**
+ * @param string $search
+ * @param bool $lookup
+ * @param int|null $limit
+ * @param int|null $offset
+ * @throws \OCP\AppFramework\QueryException
+ */
+ public function search($search, array $shareTypes, $lookup, $limit, $offset): array {
+ $hasMoreResults = false;
+
+ // Trim leading and trailing whitespace characters, e.g. when query is copy-pasted
+ $search = trim($search);
+
+ /** @var ISearchResult $searchResult */
+ $searchResult = $this->container->resolve(SearchResult::class);
+
+ foreach ($shareTypes as $type) {
+ if (!isset($this->pluginList[$type])) {
+ continue;
+ }
+ foreach ($this->pluginList[$type] as $plugin) {
+ /** @var ISearchPlugin $searchPlugin */
+ $searchPlugin = $this->container->resolve($plugin);
+ $hasMoreResults = $searchPlugin->search($search, $limit, $offset, $searchResult) || $hasMoreResults;
+ }
+ }
+
+ // Get from lookup server, not a separate share type
+ if ($lookup) {
+ $searchPlugin = $this->container->resolve(LookupPlugin::class);
+ $hasMoreResults = $searchPlugin->search($search, $limit, $offset, $searchResult) || $hasMoreResults;
+ }
+
+ // sanitizing, could go into the plugins as well
+
+ // if we have an exact match, either for the federated cloud id or for the
+ // email address, we only return the exact match. It is highly unlikely
+ // that the exact same email address and federated cloud id exists
+ $emailType = new SearchResultType('emails');
+ $remoteType = new SearchResultType('remotes');
+ if ($searchResult->hasExactIdMatch($emailType) && !$searchResult->hasExactIdMatch($remoteType)) {
+ $searchResult->unsetResult($remoteType);
+ } elseif (!$searchResult->hasExactIdMatch($emailType) && $searchResult->hasExactIdMatch($remoteType)) {
+ $searchResult->unsetResult($emailType);
+ }
+
+ $this->dropMailSharesWhereRemoteShareIsPossible($searchResult);
+
+ // if we have an exact local user match with an email-a-like query,
+ // there is no need to show the remote and email matches.
+ $userType = new SearchResultType('users');
+ if (str_contains($search, '@') && $searchResult->hasExactIdMatch($userType)) {
+ $searchResult->unsetResult($remoteType);
+ $searchResult->unsetResult($emailType);
+ }
+
+ return [$searchResult->asArray(), $hasMoreResults];
+ }
+
+ public function registerPlugin(array $pluginInfo): void {
+ $shareType = constant(IShare::class . '::' . substr($pluginInfo['shareType'], strlen('SHARE_')));
+ if ($shareType === null) {
+ throw new \InvalidArgumentException('Provided ShareType is invalid');
+ }
+ $this->pluginList[$shareType][] = $pluginInfo['class'];
+ }
+
+ protected function dropMailSharesWhereRemoteShareIsPossible(ISearchResult $searchResult): void {
+ $allResults = $searchResult->asArray();
+
+ $emailType = new SearchResultType('emails');
+ $remoteType = new SearchResultType('remotes');
+
+ if (!isset($allResults[$remoteType->getLabel()])
+ || !isset($allResults[$emailType->getLabel()])) {
+ return;
+ }
+
+ $mailIdMap = [];
+ foreach ($allResults[$emailType->getLabel()] as $mailRow) {
+ // sure, array_reduce looks nicer, but foreach needs less resources and is faster
+ if (!isset($mailRow['uuid'])) {
+ continue;
+ }
+ $mailIdMap[$mailRow['uuid']] = $mailRow['value']['shareWith'];
+ }
+
+ foreach ($allResults[$remoteType->getLabel()] as $resultRow) {
+ if (!isset($resultRow['uuid'])) {
+ continue;
+ }
+ if (isset($mailIdMap[$resultRow['uuid']])) {
+ $searchResult->removeCollaboratorResult($emailType, $mailIdMap[$resultRow['uuid']]);
+ }
+ }
+ }
+}
diff --git a/lib/private/Collaboration/Collaborators/SearchResult.php b/lib/private/Collaboration/Collaborators/SearchResult.php
new file mode 100644
index 00000000000..c9c2f032f36
--- /dev/null
+++ b/lib/private/Collaboration/Collaborators/SearchResult.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Collaboration\Collaborators;
+
+use OCP\Collaboration\Collaborators\ISearchResult;
+use OCP\Collaboration\Collaborators\SearchResultType;
+
+class SearchResult implements ISearchResult {
+ protected array $result = [
+ 'exact' => [],
+ ];
+
+ protected array $exactIdMatches = [];
+
+ public function addResultSet(SearchResultType $type, array $matches, ?array $exactMatches = null): void {
+ $type = $type->getLabel();
+ if (!isset($this->result[$type])) {
+ $this->result[$type] = [];
+ $this->result['exact'][$type] = [];
+ }
+
+ $this->result[$type] = array_merge($this->result[$type], $matches);
+ if (is_array($exactMatches)) {
+ $this->result['exact'][$type] = array_merge($this->result['exact'][$type], $exactMatches);
+ }
+ }
+
+ public function markExactIdMatch(SearchResultType $type): void {
+ $this->exactIdMatches[$type->getLabel()] = 1;
+ }
+
+ public function hasExactIdMatch(SearchResultType $type): bool {
+ return isset($this->exactIdMatches[$type->getLabel()]);
+ }
+
+ public function hasResult(SearchResultType $type, $collaboratorId): bool {
+ $type = $type->getLabel();
+ if (!isset($this->result[$type])) {
+ return false;
+ }
+
+ $resultArrays = [$this->result['exact'][$type], $this->result[$type]];
+ foreach ($resultArrays as $resultArray) {
+ foreach ($resultArray as $result) {
+ if ($result['value']['shareWith'] === $collaboratorId) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public function asArray(): array {
+ return $this->result;
+ }
+
+ public function unsetResult(SearchResultType $type): void {
+ $type = $type->getLabel();
+ $this->result[$type] = [];
+ if (isset($this->result['exact'][$type])) {
+ $this->result['exact'][$type] = [];
+ }
+ }
+
+ public function removeCollaboratorResult(SearchResultType $type, string $collaboratorId): bool {
+ $type = $type->getLabel();
+ if (!isset($this->result[$type])) {
+ return false;
+ }
+
+ $actionDone = false;
+ $resultArrays = [&$this->result['exact'][$type], &$this->result[$type]];
+ foreach ($resultArrays as &$resultArray) {
+ foreach ($resultArray as $k => $result) {
+ if ($result['value']['shareWith'] === $collaboratorId) {
+ unset($resultArray[$k]);
+ $actionDone = true;
+ }
+ }
+ }
+
+ return $actionDone;
+ }
+}
diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php
new file mode 100644
index 00000000000..671181aea35
--- /dev/null
+++ b/lib/private/Collaboration/Collaborators/UserPlugin.php
@@ -0,0 +1,266 @@
+<?php
+
+/**
+ * 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\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Share\IShare;
+use OCP\UserStatus\IManager as IUserStatusManager;
+
+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 = [],
+ ) {
+ $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;
+
+ $currentUserId = $this->userSession->getUser()->getUID();
+ $currentUserGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
+
+ // ShareWithGroupOnly filtering
+ $currentUserGroups = array_diff($currentUserGroups, $this->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;
+ }
+ }
+
+ if (!$this->shareWithGroupOnly && $this->shareeEnumerationPhone) {
+ $usersTmp = $this->userManager->searchKnownUsersByDisplayName($currentUserId, $search, $limit, $offset);
+ if (!empty($usersTmp)) {
+ foreach ($usersTmp as $user) {
+ if ($user->isEnabled()) { // Don't keep deactivated users
+ $users[$user->getUID()] = $user;
+ }
+ }
+
+ uasort($users, function ($a, $b) {
+ /**
+ * @var \OC\User\User $a
+ * @var \OC\User\User $b
+ */
+ return strcasecmp($a->getDisplayName(), $b->getDisplayName());
+ });
+ }
+ }
+ } else {
+ // Search in all users
+ if ($this->shareeEnumerationPhone) {
+ $usersTmp = $this->userManager->searchKnownUsersByDisplayName($currentUserId, $search, $limit, $offset);
+ } else {
+ $usersTmp = $this->userManager->searchDisplayName($search, $limit, $offset);
+ }
+ foreach ($usersTmp as $user) {
+ if ($user->isEnabled()) { // Don't keep deactivated users
+ $users[$user->getUID()] = $user;
+ }
+ }
+ }
+
+ $this->takeOutCurrentUser($users);
+
+ if (!$this->shareeEnumeration || count($users) < $limit) {
+ $hasMoreResults = true;
+ }
+
+ $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,
+ ];
+ }
+
+
+ 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 ($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 ($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;
+
+ if ($this->shareWithGroupOnly) {
+ // Only add, if we have a common group
+ $commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user));
+ $addUser = !empty($commonGroups);
+ }
+
+ 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['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,
+ ];
+ }
+ }
+ }
+
+ $type = new SearchResultType('users');
+ $searchResult->addResultSet($type, $result['wide'], $result['exact']);
+ if (count($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()]);
+ }
+ }
+ }
+}