summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoas Schilling <coding@schilljs.com>2021-03-17 09:02:37 +0100
committerJoas Schilling <coding@schilljs.com>2021-03-17 09:51:31 +0100
commit9a189bc710a971bea5edb4807914a3978356e66b (patch)
treec9451bc47f58b1eb61459b1d7a7b6006ec65d095
parentf82edda9c190462132f5042a177e7fa413291330 (diff)
downloadnextcloud-server-9a189bc710a971bea5edb4807914a3978356e66b.tar.gz
nextcloud-server-9a189bc710a971bea5edb4807914a3978356e66b.zip
Improve search results when only phonebook-matches can we autocompleted
Signed-off-by: Joas Schilling <coding@schilljs.com>
-rw-r--r--lib/composer/composer/autoload_classmap.php1
-rw-r--r--lib/composer/composer/autoload_static.php1
-rw-r--r--lib/private/Collaboration/Collaborators/UserPlugin.php6
-rw-r--r--lib/private/User/Database.php44
-rw-r--r--lib/private/User/Manager.php36
-rw-r--r--lib/public/IUserManager.php12
-rw-r--r--lib/public/User/Backend/IGetRealUIDBackend.php2
-rw-r--r--lib/public/User/Backend/ISearchKnownUsersBackend.php43
-rw-r--r--tests/lib/Collaboration/Collaborators/UserPluginTest.php55
9 files changed, 189 insertions, 11 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 65f050ed265..3a7cbec6172 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -534,6 +534,7 @@ return array(
'OCP\\User\\Backend\\IGetRealUIDBackend' => $baseDir . '/lib/public/User/Backend/IGetRealUIDBackend.php',
'OCP\\User\\Backend\\IPasswordConfirmationBackend' => $baseDir . '/lib/public/User/Backend/IPasswordConfirmationBackend.php',
'OCP\\User\\Backend\\IProvideAvatarBackend' => $baseDir . '/lib/public/User/Backend/IProvideAvatarBackend.php',
+ 'OCP\\User\\Backend\\ISearchKnownUsersBackend' => $baseDir . '/lib/public/User/Backend/ISearchKnownUsersBackend.php',
'OCP\\User\\Backend\\ISetDisplayNameBackend' => $baseDir . '/lib/public/User/Backend/ISetDisplayNameBackend.php',
'OCP\\User\\Backend\\ISetPasswordBackend' => $baseDir . '/lib/public/User/Backend/ISetPasswordBackend.php',
'OCP\\User\\Events\\BeforePasswordUpdatedEvent' => $baseDir . '/lib/public/User/Events/BeforePasswordUpdatedEvent.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index de07f1831bb..856ef60eef5 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -563,6 +563,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\User\\Backend\\IGetRealUIDBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/IGetRealUIDBackend.php',
'OCP\\User\\Backend\\IPasswordConfirmationBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/IPasswordConfirmationBackend.php',
'OCP\\User\\Backend\\IProvideAvatarBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/IProvideAvatarBackend.php',
+ 'OCP\\User\\Backend\\ISearchKnownUsersBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/ISearchKnownUsersBackend.php',
'OCP\\User\\Backend\\ISetDisplayNameBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/ISetDisplayNameBackend.php',
'OCP\\User\\Backend\\ISetPasswordBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/ISetPasswordBackend.php',
'OCP\\User\\Events\\BeforePasswordUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforePasswordUpdatedEvent.php',
diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php
index 06a8c6f0efd..c2132048b2f 100644
--- a/lib/private/Collaboration/Collaborators/UserPlugin.php
+++ b/lib/private/Collaboration/Collaborators/UserPlugin.php
@@ -116,7 +116,11 @@ class UserPlugin implements ISearchPlugin {
}
} else {
// Search in all users
- $usersTmp = $this->userManager->searchDisplayName($search, $limit, $offset);
+ 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;
diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php
index 85e22d196e4..0534d38fd47 100644
--- a/lib/private/User/Database.php
+++ b/lib/private/User/Database.php
@@ -70,6 +70,7 @@ use OCP\User\Backend\ICreateUserBackend;
use OCP\User\Backend\IGetDisplayNameBackend;
use OCP\User\Backend\IGetHomeBackend;
use OCP\User\Backend\IGetRealUIDBackend;
+use OCP\User\Backend\ISearchKnownUsersBackend;
use OCP\User\Backend\ISetDisplayNameBackend;
use OCP\User\Backend\ISetPasswordBackend;
@@ -84,6 +85,7 @@ class Database extends ABackend implements
ICheckPasswordBackend,
IGetHomeBackend,
ICountUsersBackend,
+ ISearchKnownUsersBackend,
IGetRealUIDBackend {
/** @var CappedMemoryCache */
private $cache;
@@ -277,7 +279,47 @@ class Database extends ABackend implements
->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
->orderBy($query->func()->lower('displayname'), 'ASC')
- ->orderBy('uid_lower', 'ASC')
+ ->addOrderBy('uid_lower', 'ASC')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset);
+
+ $result = $query->execute();
+ $displayNames = [];
+ while ($row = $result->fetch()) {
+ $displayNames[(string)$row['uid']] = (string)$row['displayname'];
+ }
+
+ return $displayNames;
+ }
+
+ /**
+ * @param string $searcher
+ * @param string $pattern
+ * @param int|null $limit
+ * @param int|null $offset
+ * @return array
+ * @since 21.0.1
+ */
+ public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
+ $limit = $this->fixLimit($limit);
+
+ $this->fixDI();
+
+ $query = $this->dbConn->getQueryBuilder();
+
+ $query->select('u.uid', 'u.displayname')
+ ->from($this->table, 'u')
+ ->leftJoin('u', 'known_users', 'k', $query->expr()->andX(
+ $query->expr()->eq('k.known_user', 'u.uid'),
+ $query->expr()->eq('k.known_to', $query->createNamedParameter($searcher))
+ ))
+ ->where($query->expr()->eq('k.known_to', $query->createNamedParameter($searcher)))
+ ->andWhere($query->expr()->orX(
+ $query->expr()->iLike('u.uid', $query->createNamedParameter('%' . $this->dbConn->escapeLikeParameter($pattern) . '%')),
+ $query->expr()->iLike('u.displayname', $query->createNamedParameter('%' . $this->dbConn->escapeLikeParameter($pattern) . '%'))
+ ))
+ ->orderBy('u.displayname', 'ASC')
+ ->addOrderBy('u.uid_lower', 'ASC')
->setMaxResults($limit)
->setFirstResult($offset);
diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php
index 036d2703d35..64aa5e1d22b 100644
--- a/lib/private/User/Manager.php
+++ b/lib/private/User/Manager.php
@@ -47,6 +47,7 @@ use OCP\IUserBackend;
use OCP\IUserManager;
use OCP\Support\Subscription\IRegistry;
use OCP\User\Backend\IGetRealUIDBackend;
+use OCP\User\Backend\ISearchKnownUsersBackend;
use OCP\User\Events\BeforeUserCreatedEvent;
use OCP\User\Events\UserCreatedEvent;
use OCP\UserInterface;
@@ -330,6 +331,41 @@ class Manager extends PublicEmitter implements IUserManager {
}
/**
+ * Search known users (from phonebook sync) by displayName
+ *
+ * @param string $searcher
+ * @param string $pattern
+ * @param int|null $limit
+ * @param int|null $offset
+ * @return IUser[]
+ */
+ public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
+ $users = [];
+ foreach ($this->backends as $backend) {
+ if ($backend instanceof ISearchKnownUsersBackend) {
+ $backendUsers = $backend->searchKnownUsersByDisplayName($searcher, $pattern, $limit, $offset);
+ } else {
+ // Better than nothing, but filtering after pagination can remove lots of results.
+ $backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
+ }
+ if (is_array($backendUsers)) {
+ foreach ($backendUsers as $uid => $displayName) {
+ $users[] = $this->getUserObject($uid, $backend);
+ }
+ }
+ }
+
+ usort($users, function ($a, $b) {
+ /**
+ * @var IUser $a
+ * @var IUser $b
+ */
+ return strcasecmp($a->getDisplayName(), $b->getDisplayName());
+ });
+ return $users;
+ }
+
+ /**
* @param string $uid
* @param string $password
* @throws \InvalidArgumentException
diff --git a/lib/public/IUserManager.php b/lib/public/IUserManager.php
index e8a7fc7827d..6963bb5ddbc 100644
--- a/lib/public/IUserManager.php
+++ b/lib/public/IUserManager.php
@@ -127,6 +127,18 @@ interface IUserManager {
public function searchDisplayName($pattern, $limit = null, $offset = null);
/**
+ * Search known users (from phonebook sync) by displayName
+ *
+ * @param string $searcher
+ * @param string $pattern
+ * @param int|null $limit
+ * @param int|null $offset
+ * @return IUser[]
+ * @since 21.0.1
+ */
+ public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array;
+
+ /**
* @param string $uid
* @param string $password
* @throws \InvalidArgumentException
diff --git a/lib/public/User/Backend/IGetRealUIDBackend.php b/lib/public/User/Backend/IGetRealUIDBackend.php
index d724426e176..cc290eb6dc0 100644
--- a/lib/public/User/Backend/IGetRealUIDBackend.php
+++ b/lib/public/User/Backend/IGetRealUIDBackend.php
@@ -33,7 +33,7 @@ interface IGetRealUIDBackend {
/**
* Some backends accept different UIDs than what is the internal UID to be used.
- * For example the database backend accepts differnt cased UIDs in all the functions
+ * For example the database backend accepts different cased UIDs in all the functions
* but the internal UID that is to be used should be correctly cased.
*
* This little function makes sure that the used UID will be correct hen using the user object
diff --git a/lib/public/User/Backend/ISearchKnownUsersBackend.php b/lib/public/User/Backend/ISearchKnownUsersBackend.php
new file mode 100644
index 00000000000..89c7a49cd30
--- /dev/null
+++ b/lib/public/User/Backend/ISearchKnownUsersBackend.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
+ *
+ * @author Joas Schilling <coding@schilljs.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\User\Backend;
+
+/**
+ * @since 21.0.1
+ */
+interface ISearchKnownUsersBackend {
+
+ /**
+ * @param string $searcher
+ * @param string $pattern
+ * @param int|null $limit
+ * @param int|null $offset
+ * @return array
+ * @since 21.0.1
+ */
+ public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array;
+}
diff --git a/tests/lib/Collaboration/Collaborators/UserPluginTest.php b/tests/lib/Collaboration/Collaborators/UserPluginTest.php
index f2e0e7e274b..43bbffc9b6a 100644
--- a/tests/lib/Collaboration/Collaborators/UserPluginTest.php
+++ b/tests/lib/Collaboration/Collaborators/UserPluginTest.php
@@ -104,17 +104,19 @@ class UserPluginTest extends TestCase {
);
}
- public function mockConfig($shareWithGroupOnly, $shareeEnumeration, $shareeEnumerationLimitToGroup) {
+ public function mockConfig($shareWithGroupOnly, $shareeEnumeration, $shareeEnumerationLimitToGroup, $shareeEnumerationPhone = false) {
$this->config->expects($this->any())
->method('getAppValue')
->willReturnCallback(
- function ($appName, $key, $default) use ($shareWithGroupOnly, $shareeEnumeration, $shareeEnumerationLimitToGroup) {
+ function ($appName, $key, $default) use ($shareWithGroupOnly, $shareeEnumeration, $shareeEnumerationLimitToGroup, $shareeEnumerationPhone) {
if ($appName === 'core' && $key === 'shareapi_only_share_with_group_members') {
return $shareWithGroupOnly ? 'yes' : 'no';
} elseif ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') {
return $shareeEnumeration ? 'yes' : 'no';
} elseif ($appName === 'core' && $key === 'shareapi_restrict_user_enumeration_to_group') {
return $shareeEnumerationLimitToGroup ? 'yes' : 'no';
+ } elseif ($appName === 'core' && $key === 'shareapi_restrict_user_enumeration_to_phone') {
+ return $shareeEnumerationPhone ? 'yes' : 'no';
}
return $default;
}
@@ -269,6 +271,28 @@ class UserPluginTest extends TestCase {
[
'test',
false,
+ true,
+ [],
+ [
+ $this->getUserMock('test0', 'Test'),
+ $this->getUserMock('test1', 'Test One'),
+ $this->getUserMock('test2', 'Test Two'),
+ ],
+ [
+ ['label' => 'Test', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test0'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test0'],
+ ],
+ [
+ ['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test1'],
+ ['label' => 'Test Two', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2'], 'icon' => 'icon-user', 'subline' => null, 'status' => [], 'shareWithDisplayNameUnique' => 'test2'],
+ ],
+ false,
+ false,
+ [],
+ true,
+ ],
+ [
+ 'test',
+ false,
false,
[],
[
@@ -443,9 +467,10 @@ class UserPluginTest extends TestCase {
array $expected,
$reachedEnd,
$singleUser,
- array $users = []
+ array $users = [],
+ $shareeEnumerationPhone = false
) {
- $this->mockConfig($shareWithGroupOnly, $shareeEnumeration, false);
+ $this->mockConfig($shareWithGroupOnly, $shareeEnumeration, false, $shareeEnumerationPhone);
$this->instantiatePlugin();
$this->session->expects($this->any())
@@ -453,10 +478,24 @@ class UserPluginTest extends TestCase {
->willReturn($this->user);
if (!$shareWithGroupOnly) {
- $this->userManager->expects($this->once())
- ->method('searchDisplayName')
- ->with($searchTerm, $this->limit, $this->offset)
- ->willReturn($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],
+ ]);
+ } else {
+ $this->userManager->expects($this->once())
+ ->method('searchDisplayName')
+ ->with($searchTerm, $this->limit, $this->offset)
+ ->willReturn($userResponse);
+ }
} else {
$this->groupManager->method('getUserGroupIds')
->with($this->user)