Merge pull request #26031 from nextcloud/feature/noid/allow-autocomplete-based-on-phone-sync

Allow autocomplete based on phone sync
This commit is contained in:
Joas Schilling 2021-03-11 08:29:21 +01:00 committed by GitHub
commit 56b08c04c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1619 additions and 348 deletions

View File

@ -754,6 +754,31 @@ trigger:
- pull_request - pull_request
- push - push
---
kind: pipeline
name: integration-collaboration_features
steps:
- name: submodules
image: docker:git
commands:
- git submodule update --init
- name: integration-collaboration_features
image: nextcloudci/integration-php7.3:integration-php7.3-2
commands:
- bash tests/drone-run-integration-tests.sh || exit 0
- ./occ maintenance:install --admin-pass=admin --data-dir=/dev/shm/nc_int
- cd build/integration
- ./run.sh collaboration_features/
trigger:
branch:
- master
- stable*
event:
- pull_request
- push
--- ---
kind: pipeline kind: pipeline
name: integration-federation_features name: integration-federation_features

View File

@ -27,6 +27,7 @@
*/ */
// Backends // Backends
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Connector\LegacyDAVACL; use OCA\DAV\Connector\LegacyDAVACL;
use OCA\DAV\CalDAV\CalendarRoot; use OCA\DAV\CalDAV\CalendarRoot;
@ -50,6 +51,7 @@ $principalBackend = new Principal(
\OC::$server->getUserSession(), \OC::$server->getUserSession(),
\OC::$server->getAppManager(), \OC::$server->getAppManager(),
\OC::$server->query(\OCA\DAV\CalDAV\Proxy\ProxyMapper::class), \OC::$server->query(\OCA\DAV\CalDAV\Proxy\ProxyMapper::class),
\OC::$server->get(KnownUserService::class),
\OC::$server->getConfig(), \OC::$server->getConfig(),
'principals/' 'principals/'
); );

View File

@ -27,6 +27,7 @@
*/ */
// Backends // Backends
use OC\KnownUser\KnownUserService;
use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CardDAV\AddressBookRoot; use OCA\DAV\CardDAV\AddressBookRoot;
use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\CardDavBackend;
@ -53,6 +54,7 @@ $principalBackend = new Principal(
\OC::$server->getUserSession(), \OC::$server->getUserSession(),
\OC::$server->getAppManager(), \OC::$server->getAppManager(),
\OC::$server->query(\OCA\DAV\CalDAV\Proxy\ProxyMapper::class), \OC::$server->query(\OCA\DAV\CalDAV\Proxy\ProxyMapper::class),
\OC::$server->get(KnownUserService::class),
\OC::$server->getConfig(), \OC::$server->getConfig(),
'principals/' 'principals/'
); );

View File

@ -43,8 +43,9 @@ class SystemAddressbook extends AddressBook {
public function getChildren() { public function getChildren() {
$shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
$restrictShareEnumeration = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
if (!$shareEnumeration || ($shareEnumeration && $restrictShareEnumeration)) { $shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
if (!$shareEnumeration || $shareEnumerationGroup || $shareEnumerationPhone) {
return []; return [];
} }

View File

@ -27,6 +27,7 @@
namespace OCA\DAV\Command; namespace OCA\DAV\Command;
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\Connector\Sabre\Principal;
@ -86,6 +87,7 @@ class CreateCalendar extends Command {
\OC::$server->getUserSession(), \OC::$server->getUserSession(),
\OC::$server->getAppManager(), \OC::$server->getAppManager(),
\OC::$server->query(ProxyMapper::class), \OC::$server->query(ProxyMapper::class),
\OC::$server->get(KnownUserService::class),
\OC::$server->getConfig() \OC::$server->getConfig()
); );
$random = \OC::$server->getSecureRandom(); $random = \OC::$server->getSecureRandom();

View File

@ -36,6 +36,7 @@
namespace OCA\DAV\Connector\Sabre; namespace OCA\DAV\Connector\Sabre;
use OC\KnownUser\KnownUserService;
use OCA\Circles\Exceptions\CircleDoesNotExistException; use OCA\Circles\Exceptions\CircleDoesNotExistException;
use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Traits\PrincipalProxyTrait; use OCA\DAV\Traits\PrincipalProxyTrait;
@ -82,27 +83,19 @@ class Principal implements BackendInterface {
/** @var ProxyMapper */ /** @var ProxyMapper */
private $proxyMapper; private $proxyMapper;
/** @var KnownUserService */
private $knownUserService;
/** @var IConfig */ /** @var IConfig */
private $config; private $config;
/**
* Principal constructor.
*
* @param IUserManager $userManager
* @param IGroupManager $groupManager
* @param IShareManager $shareManager
* @param IUserSession $userSession
* @param IAppManager $appManager
* @param ProxyMapper $proxyMapper
* @param IConfig $config
* @param string $principalPrefix
*/
public function __construct(IUserManager $userManager, public function __construct(IUserManager $userManager,
IGroupManager $groupManager, IGroupManager $groupManager,
IShareManager $shareManager, IShareManager $shareManager,
IUserSession $userSession, IUserSession $userSession,
IAppManager $appManager, IAppManager $appManager,
ProxyMapper $proxyMapper, ProxyMapper $proxyMapper,
KnownUserService $knownUserService,
IConfig $config, IConfig $config,
string $principalPrefix = 'principals/users/') { string $principalPrefix = 'principals/users/') {
$this->userManager = $userManager; $this->userManager = $userManager;
@ -113,6 +106,7 @@ class Principal implements BackendInterface {
$this->principalPrefix = trim($principalPrefix, '/'); $this->principalPrefix = trim($principalPrefix, '/');
$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/'); $this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
$this->proxyMapper = $proxyMapper; $this->proxyMapper = $proxyMapper;
$this->knownUserService = $knownUserService;
$this->config = $config; $this->config = $config;
} }
@ -267,24 +261,25 @@ class Principal implements BackendInterface {
} }
$allowEnumeration = $this->shareManager->allowEnumeration(); $allowEnumeration = $this->shareManager->allowEnumeration();
$limitEnumeration = $this->shareManager->limitEnumerationToGroups(); $limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
$limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
$allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
// If sharing is restricted to group members only, // If sharing is restricted to group members only,
// return only members that have groups in common // return only members that have groups in common
$restrictGroups = false; $restrictGroups = false;
$currentUser = $this->userSession->getUser();
if ($this->shareManager->shareWithGroupMembersOnly()) { if ($this->shareManager->shareWithGroupMembersOnly()) {
$user = $this->userSession->getUser(); if (!$currentUser instanceof IUser) {
if (!$user) {
return []; return [];
} }
$restrictGroups = $this->groupManager->getUserGroupIds($user); $restrictGroups = $this->groupManager->getUserGroupIds($currentUser);
} }
$currentUserGroups = []; $currentUserGroups = [];
if ($limitEnumeration) { if ($limitEnumerationGroup) {
$currentUser = $this->userSession->getUser(); if ($currentUser instanceof IUser) {
if ($currentUser) {
$currentUserGroups = $this->groupManager->getUserGroupIds($currentUser); $currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
} }
} }
@ -296,20 +291,38 @@ class Principal implements BackendInterface {
foreach ($searchProperties as $prop => $value) { foreach ($searchProperties as $prop => $value) {
switch ($prop) { switch ($prop) {
case '{http://sabredav.org/ns}email-address': case '{http://sabredav.org/ns}email-address':
$users = $this->userManager->getByEmail($value);
if (!$allowEnumeration) { if (!$allowEnumeration) {
if ($allowEnumerationFullMatch) {
$users = $this->userManager->getByEmail($value);
$users = \array_filter($users, static function (IUser $user) use ($value) { $users = \array_filter($users, static function (IUser $user) use ($value) {
return $user->getEMailAddress() === $value; return $user->getEMailAddress() === $value;
}); });
} else {
$users = [];
}
} else {
$users = $this->userManager->getByEmail($value);
$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
if ($allowEnumerationFullMatch && $user->getEMailAddress() === $value) {
return true;
} }
if ($limitEnumeration) { if ($limitEnumerationPhone
$users = \array_filter($users, function (IUser $user) use ($currentUserGroups, $value) { && $currentUser instanceof IUser
return !empty(array_intersect( && $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
// Synced phonebook match
return true;
}
if (!$limitEnumerationGroup) {
// No limitation on enumeration, all allowed
return true;
}
return !empty($currentUserGroups) && !empty(array_intersect(
$this->groupManager->getUserGroupIds($user), $this->groupManager->getUserGroupIds($user),
$currentUserGroups $currentUserGroups
)) || $user->getEMailAddress() === $value; ));
}); });
} }
@ -328,20 +341,39 @@ class Principal implements BackendInterface {
break; break;
case '{DAV:}displayname': case '{DAV:}displayname':
$users = $this->userManager->searchDisplayName($value, $searchLimit);
if (!$allowEnumeration) { if (!$allowEnumeration) {
if ($allowEnumerationFullMatch) {
$users = $this->userManager->searchDisplayName($value, $searchLimit);
$users = \array_filter($users, static function (IUser $user) use ($value) { $users = \array_filter($users, static function (IUser $user) use ($value) {
return $user->getDisplayName() === $value; return $user->getDisplayName() === $value;
}); });
} else {
$users = [];
}
} else {
$users = $this->userManager->searchDisplayName($value, $searchLimit);
$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
return true;
} }
if ($limitEnumeration) { if ($limitEnumerationPhone
$users = \array_filter($users, function (IUser $user) use ($currentUserGroups, $value) { && $currentUser instanceof IUser
return !empty(array_intersect( && $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
// Synced phonebook match
return true;
}
if (!$limitEnumerationGroup) {
// No limitation on enumeration, all allowed
return true;
}
return !empty($currentUserGroups) && !empty(array_intersect(
$this->groupManager->getUserGroupIds($user), $this->groupManager->getUserGroupIds($user),
$currentUserGroups $currentUserGroups
)) || $user->getDisplayName() === $value; ));
}); });
} }

View File

@ -28,6 +28,7 @@
namespace OCA\DAV; namespace OCA\DAV;
use OC\KnownUser\KnownUserService;
use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\CalendarRoot; use OCA\DAV\CalDAV\CalendarRoot;
@ -70,6 +71,7 @@ class RootCollection extends SimpleCollection {
\OC::$server->getUserSession(), \OC::$server->getUserSession(),
\OC::$server->getAppManager(), \OC::$server->getAppManager(),
$proxyMapper, $proxyMapper,
\OC::$server->get(KnownUserService::class),
\OC::$server->getConfig() \OC::$server->getConfig()
); );
$groupPrincipalBackend = new GroupPrincipalBackend($groupManager, $userSession, $shareManager, $config); $groupPrincipalBackend = new GroupPrincipalBackend($groupManager, $userSession, $shareManager, $config);

View File

@ -27,6 +27,7 @@
namespace OCA\DAV\Tests\unit\CalDAV; namespace OCA\DAV\Tests\unit\CalDAV;
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\Connector\Sabre\Principal;
@ -92,6 +93,7 @@ abstract class AbstractCalDavBackend extends TestCase {
$this->createMock(IUserSession::class), $this->createMock(IUserSession::class),
$this->createMock(IAppManager::class), $this->createMock(IAppManager::class),
$this->createMock(ProxyMapper::class), $this->createMock(ProxyMapper::class),
$this->createMock(KnownUserService::class),
$this->createMock(IConfig::class), $this->createMock(IConfig::class),
]) ])
->setMethods(['getPrincipalByPath', 'getGroupMembership']) ->setMethods(['getPrincipalByPath', 'getGroupMembership'])

View File

@ -33,6 +33,7 @@
namespace OCA\DAV\Tests\unit\CardDAV; namespace OCA\DAV\Tests\unit\CardDAV;
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\CardDAV\AddressBook; use OCA\DAV\CardDAV\AddressBook;
use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\CardDavBackend;
@ -139,6 +140,7 @@ class CardDavBackendTest extends TestCase {
$this->createMock(IUserSession::class), $this->createMock(IUserSession::class),
$this->createMock(IAppManager::class), $this->createMock(IAppManager::class),
$this->createMock(ProxyMapper::class), $this->createMock(ProxyMapper::class),
$this->createMock(KnownUserService::class),
$this->createMock(IConfig::class), $this->createMock(IConfig::class),
]) ])
->setMethods(['getPrincipalByPath', 'getGroupMembership']) ->setMethods(['getPrincipalByPath', 'getGroupMembership'])

View File

@ -30,6 +30,7 @@
namespace OCA\DAV\Tests\unit\Connector\Sabre; namespace OCA\DAV\Tests\unit\Connector\Sabre;
use OC\KnownUser\KnownUserService;
use OC\User\User; use OC\User\User;
use OCA\DAV\CalDAV\Proxy\Proxy; use OCA\DAV\CalDAV\Proxy\Proxy;
use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Proxy\ProxyMapper;
@ -41,6 +42,7 @@ use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\Share\IManager; use OCP\Share\IManager;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\PropPatch; use Sabre\DAV\PropPatch;
use Test\TestCase; use Test\TestCase;
@ -67,6 +69,8 @@ class PrincipalTest extends TestCase {
/** @var ProxyMapper | \PHPUnit\Framework\MockObject\MockObject */ /** @var ProxyMapper | \PHPUnit\Framework\MockObject\MockObject */
private $proxyMapper; private $proxyMapper;
/** @var KnownUserService|MockObject */
private $knownUserService;
/** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */ /** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */
private $config; private $config;
@ -77,6 +81,7 @@ class PrincipalTest extends TestCase {
$this->userSession = $this->createMock(IUserSession::class); $this->userSession = $this->createMock(IUserSession::class);
$this->appManager = $this->createMock(IAppManager::class); $this->appManager = $this->createMock(IAppManager::class);
$this->proxyMapper = $this->createMock(ProxyMapper::class); $this->proxyMapper = $this->createMock(ProxyMapper::class);
$this->knownUserService = $this->createMock(KnownUserService::class);
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->connector = new \OCA\DAV\Connector\Sabre\Principal( $this->connector = new \OCA\DAV\Connector\Sabre\Principal(
@ -86,6 +91,7 @@ class PrincipalTest extends TestCase {
$this->userSession, $this->userSession,
$this->appManager, $this->appManager,
$this->proxyMapper, $this->proxyMapper,
$this->knownUserService,
$this->config $this->config
); );
parent::setUp(); parent::setUp();
@ -442,7 +448,7 @@ class PrincipalTest extends TestCase {
if ($groupsOnly) { if ($groupsOnly) {
$user = $this->createMock(IUser::class); $user = $this->createMock(IUser::class);
$this->userSession->expects($this->once()) $this->userSession->expects($this->atLeastOnce())
->method('getUser') ->method('getUser')
->willReturn($user); ->willReturn($user);
@ -564,6 +570,10 @@ class PrincipalTest extends TestCase {
->method('shareWithGroupMembersOnly') ->method('shareWithGroupMembersOnly')
->willReturn(false); ->willReturn(false);
$this->shareManager->expects($this->once())
->method('allowEnumerationFullMatch')
->willReturn(true);
$user2 = $this->createMock(IUser::class); $user2 = $this->createMock(IUser::class);
$user2->method('getUID')->willReturn('user2'); $user2->method('getUID')->willReturn('user2');
$user2->method('getDisplayName')->willReturn('User 2'); $user2->method('getDisplayName')->willReturn('User 2');
@ -586,6 +596,27 @@ class PrincipalTest extends TestCase {
['{DAV:}displayname' => 'User 2'])); ['{DAV:}displayname' => 'User 2']));
} }
public function testSearchPrincipalWithEnumerationDisabledDisplaynameOnFullMatch() {
$this->shareManager->expects($this->once())
->method('shareAPIEnabled')
->willReturn(true);
$this->shareManager->expects($this->once())
->method('allowEnumeration')
->willReturn(false);
$this->shareManager->expects($this->once())
->method('shareWithGroupMembersOnly')
->willReturn(false);
$this->shareManager->expects($this->once())
->method('allowEnumerationFullMatch')
->willReturn(false);
$this->assertEquals([], $this->connector->searchPrincipals('principals/users',
['{DAV:}displayname' => 'User 2']));
}
public function testSearchPrincipalWithEnumerationDisabledEmail() { public function testSearchPrincipalWithEnumerationDisabledEmail() {
$this->shareManager->expects($this->once()) $this->shareManager->expects($this->once())
->method('shareAPIEnabled') ->method('shareAPIEnabled')
@ -599,6 +630,10 @@ class PrincipalTest extends TestCase {
->method('shareWithGroupMembersOnly') ->method('shareWithGroupMembersOnly')
->willReturn(false); ->willReturn(false);
$this->shareManager->expects($this->once())
->method('allowEnumerationFullMatch')
->willReturn(true);
$user2 = $this->createMock(IUser::class); $user2 = $this->createMock(IUser::class);
$user2->method('getUID')->willReturn('user2'); $user2->method('getUID')->willReturn('user2');
$user2->method('getDisplayName')->willReturn('User 2'); $user2->method('getDisplayName')->willReturn('User 2');
@ -621,6 +656,28 @@ class PrincipalTest extends TestCase {
['{http://sabredav.org/ns}email-address' => 'user2@foo.bar'])); ['{http://sabredav.org/ns}email-address' => 'user2@foo.bar']));
} }
public function testSearchPrincipalWithEnumerationDisabledEmailOnFullMatch() {
$this->shareManager->expects($this->once())
->method('shareAPIEnabled')
->willReturn(true);
$this->shareManager->expects($this->once())
->method('allowEnumeration')
->willReturn(false);
$this->shareManager->expects($this->once())
->method('shareWithGroupMembersOnly')
->willReturn(false);
$this->shareManager->expects($this->once())
->method('allowEnumerationFullMatch')
->willReturn(false);
$this->assertEquals([], $this->connector->searchPrincipals('principals/users',
['{http://sabredav.org/ns}email-address' => 'user2@foo.bar']));
}
public function testSearchPrincipalWithEnumerationLimitedDisplayname() { public function testSearchPrincipalWithEnumerationLimitedDisplayname() {
$this->shareManager->expects($this->at(0)) $this->shareManager->expects($this->at(0))
->method('shareAPIEnabled') ->method('shareAPIEnabled')

View File

@ -27,6 +27,7 @@
namespace OCA\Files_Versions\AppInfo; namespace OCA\Files_Versions\AppInfo;
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\Connector\Sabre\Principal;
use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files\Event\LoadAdditionalScriptsEvent;
@ -72,6 +73,7 @@ class Application extends App implements IBootstrap {
$server->getUserSession(), $server->getUserSession(),
$server->getAppManager(), $server->getAppManager(),
$server->get(ProxyMapper::class), $server->get(ProxyMapper::class),
$server->get(KnownUserService::class),
$server->getConfig() $server->getConfig()
); );
}); });

View File

@ -14,6 +14,7 @@ return array(
'OCA\\Provisioning_API\\Controller\\GroupsController' => $baseDir . '/../lib/Controller/GroupsController.php', 'OCA\\Provisioning_API\\Controller\\GroupsController' => $baseDir . '/../lib/Controller/GroupsController.php',
'OCA\\Provisioning_API\\Controller\\UsersController' => $baseDir . '/../lib/Controller/UsersController.php', 'OCA\\Provisioning_API\\Controller\\UsersController' => $baseDir . '/../lib/Controller/UsersController.php',
'OCA\\Provisioning_API\\FederatedShareProviderFactory' => $baseDir . '/../lib/FederatedShareProviderFactory.php', 'OCA\\Provisioning_API\\FederatedShareProviderFactory' => $baseDir . '/../lib/FederatedShareProviderFactory.php',
'OCA\\Provisioning_API\\Listener\\UserDeletedListener' => $baseDir . '/../lib/Listener/UserDeletedListener.php',
'OCA\\Provisioning_API\\Middleware\\Exceptions\\NotSubAdminException' => $baseDir . '/../lib/Middleware/Exceptions/NotSubAdminException.php', 'OCA\\Provisioning_API\\Middleware\\Exceptions\\NotSubAdminException' => $baseDir . '/../lib/Middleware/Exceptions/NotSubAdminException.php',
'OCA\\Provisioning_API\\Middleware\\ProvisioningApiMiddleware' => $baseDir . '/../lib/Middleware/ProvisioningApiMiddleware.php', 'OCA\\Provisioning_API\\Middleware\\ProvisioningApiMiddleware' => $baseDir . '/../lib/Middleware/ProvisioningApiMiddleware.php',
); );

View File

@ -29,6 +29,7 @@ class ComposerStaticInitProvisioning_API
'OCA\\Provisioning_API\\Controller\\GroupsController' => __DIR__ . '/..' . '/../lib/Controller/GroupsController.php', 'OCA\\Provisioning_API\\Controller\\GroupsController' => __DIR__ . '/..' . '/../lib/Controller/GroupsController.php',
'OCA\\Provisioning_API\\Controller\\UsersController' => __DIR__ . '/..' . '/../lib/Controller/UsersController.php', 'OCA\\Provisioning_API\\Controller\\UsersController' => __DIR__ . '/..' . '/../lib/Controller/UsersController.php',
'OCA\\Provisioning_API\\FederatedShareProviderFactory' => __DIR__ . '/..' . '/../lib/FederatedShareProviderFactory.php', 'OCA\\Provisioning_API\\FederatedShareProviderFactory' => __DIR__ . '/..' . '/../lib/FederatedShareProviderFactory.php',
'OCA\\Provisioning_API\\Listener\\UserDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/UserDeletedListener.php',
'OCA\\Provisioning_API\\Middleware\\Exceptions\\NotSubAdminException' => __DIR__ . '/..' . '/../lib/Middleware/Exceptions/NotSubAdminException.php', 'OCA\\Provisioning_API\\Middleware\\Exceptions\\NotSubAdminException' => __DIR__ . '/..' . '/../lib/Middleware/Exceptions/NotSubAdminException.php',
'OCA\\Provisioning_API\\Middleware\\ProvisioningApiMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/ProvisioningApiMiddleware.php', 'OCA\\Provisioning_API\\Middleware\\ProvisioningApiMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/ProvisioningApiMiddleware.php',
); );

View File

@ -29,6 +29,7 @@
namespace OCA\Provisioning_API\AppInfo; namespace OCA\Provisioning_API\AppInfo;
use OC\Group\Manager as GroupManager; use OC\Group\Manager as GroupManager;
use OCA\Provisioning_API\Listener\UserDeletedListener;
use OCA\Provisioning_API\Middleware\ProvisioningApiMiddleware; use OCA\Provisioning_API\Middleware\ProvisioningApiMiddleware;
use OCA\Settings\Mailer\NewUserMailHelper; use OCA\Settings\Mailer\NewUserMailHelper;
use OCP\AppFramework\App; use OCP\AppFramework\App;
@ -47,6 +48,7 @@ use OCP\L10N\IFactory;
use OCP\Mail\IMailer; use OCP\Mail\IMailer;
use OCP\Security\ICrypto; use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom; use OCP\Security\ISecureRandom;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util; use OCP\Util;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@ -56,6 +58,8 @@ class Application extends App implements IBootstrap {
} }
public function register(IRegistrationContext $context): void { public function register(IRegistrationContext $context): void {
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerService(NewUserMailHelper::class, function (ContainerInterface $c) { $context->registerService(NewUserMailHelper::class, function (ContainerInterface $c) {
return new NewUserMailHelper( return new NewUserMailHelper(
$c->get(Defaults::class), $c->get(Defaults::class),

View File

@ -49,6 +49,7 @@ use libphonenumber\PhoneNumberUtil;
use OC\Accounts\AccountManager; use OC\Accounts\AccountManager;
use OC\Authentication\Token\RemoteWipe; use OC\Authentication\Token\RemoteWipe;
use OC\HintException; use OC\HintException;
use OC\KnownUser\KnownUserService;
use OCA\Provisioning_API\FederatedShareProviderFactory; use OCA\Provisioning_API\FederatedShareProviderFactory;
use OCA\Settings\Mailer\NewUserMailHelper; use OCA\Settings\Mailer\NewUserMailHelper;
use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountManager;
@ -90,6 +91,8 @@ class UsersController extends AUserData {
private $secureRandom; private $secureRandom;
/** @var RemoteWipe */ /** @var RemoteWipe */
private $remoteWipe; private $remoteWipe;
/** @var KnownUserService */
private $knownUserService;
/** @var IEventDispatcher */ /** @var IEventDispatcher */
private $eventDispatcher; private $eventDispatcher;
@ -108,6 +111,7 @@ class UsersController extends AUserData {
FederatedShareProviderFactory $federatedShareProviderFactory, FederatedShareProviderFactory $federatedShareProviderFactory,
ISecureRandom $secureRandom, ISecureRandom $secureRandom,
RemoteWipe $remoteWipe, RemoteWipe $remoteWipe,
KnownUserService $knownUserService,
IEventDispatcher $eventDispatcher) { IEventDispatcher $eventDispatcher) {
parent::__construct($appName, parent::__construct($appName,
$request, $request,
@ -126,6 +130,7 @@ class UsersController extends AUserData {
$this->federatedShareProviderFactory = $federatedShareProviderFactory; $this->federatedShareProviderFactory = $federatedShareProviderFactory;
$this->secureRandom = $secureRandom; $this->secureRandom = $secureRandom;
$this->remoteWipe = $remoteWipe; $this->remoteWipe = $remoteWipe;
$this->knownUserService = $knownUserService;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
} }
@ -231,6 +236,13 @@ class UsersController extends AUserData {
return new DataResponse([], Http::STATUS_BAD_REQUEST); return new DataResponse([], Http::STATUS_BAD_REQUEST);
} }
/** @var IUser $user */
$user = $this->userSession->getUser();
$knownTo = $user->getUID();
// Cleanup all previous entries and only allow new matches
$this->knownUserService->deleteKnownTo($knownTo);
$normalizedNumberToKey = []; $normalizedNumberToKey = [];
foreach ($search as $key => $phoneNumbers) { foreach ($search as $key => $phoneNumbers) {
foreach ($phoneNumbers as $phone) { foreach ($phoneNumbers as $phone) {
@ -268,6 +280,7 @@ class UsersController extends AUserData {
foreach ($userMatches as $phone => $userId) { foreach ($userMatches as $phone => $userId) {
// Not using the ICloudIdManager as that would run a search for each contact to find the display name in the address book // Not using the ICloudIdManager as that would run a search for each contact to find the display name in the address book
$matches[$normalizedNumberToKey[$phone]] = $userId . '@' . $cloudUrl; $matches[$normalizedNumberToKey[$phone]] = $userId . '@' . $cloudUrl;
$this->knownUserService->storeIsKnownToUser($knownTo, $userId);
} }
return new DataResponse($matches); return new DataResponse($matches);
@ -677,6 +690,10 @@ class UsersController extends AUserData {
$userAccount[$key]['value'] = $value; $userAccount[$key]['value'] = $value;
try { try {
$this->accountManager->updateUser($targetUser, $userAccount, true); $this->accountManager->updateUser($targetUser, $userAccount, true);
if ($key === IAccountManager::PROPERTY_PHONE) {
$this->knownUserService->deleteByContactUserId($targetUser->getUID());
}
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
throw new OCSException('Invalid ' . $e->getMessage(), 102); throw new OCSException('Invalid ' . $e->getMessage(), 102);
} }

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 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 OCA\Provisioning_API\Listener;
use OC\KnownUser\KnownUserService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\User\Events\UserDeletedEvent;
class UserDeletedListener implements IEventListener {
/** @var KnownUserService */
private $service;
public function __construct(KnownUserService $service) {
$this->service = $service;
}
public function handle(Event $event): void {
if (!($event instanceof UserDeletedEvent)) {
// Unrelated
return;
}
$user = $event->getUser();
// Delete all entries of this user
$this->service->deleteKnownTo($user->getUID());
// Delete all entries that other users know this user
$this->service->deleteByContactUserId($user->getUID());
}
}

View File

@ -44,6 +44,7 @@ use Exception;
use OC\Accounts\AccountManager; use OC\Accounts\AccountManager;
use OC\Authentication\Token\RemoteWipe; use OC\Authentication\Token\RemoteWipe;
use OC\Group\Manager; use OC\Group\Manager;
use OC\KnownUser\KnownUserService;
use OC\SubAdmin; use OC\SubAdmin;
use OCA\FederatedFileSharing\FederatedShareProvider; use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\Provisioning_API\Controller\UsersController; use OCA\Provisioning_API\Controller\UsersController;
@ -102,6 +103,8 @@ class UsersControllerTest extends TestCase {
private $secureRandom; private $secureRandom;
/** @var RemoteWipe|MockObject */ /** @var RemoteWipe|MockObject */
private $remoteWipe; private $remoteWipe;
/** @var KnownUserService|MockObject */
private $knownUserService;
/** @var IEventDispatcher */ /** @var IEventDispatcher */
private $eventDispatcher; private $eventDispatcher;
@ -122,6 +125,7 @@ class UsersControllerTest extends TestCase {
$this->federatedShareProviderFactory = $this->createMock(FederatedShareProviderFactory::class); $this->federatedShareProviderFactory = $this->createMock(FederatedShareProviderFactory::class);
$this->secureRandom = $this->createMock(ISecureRandom::class); $this->secureRandom = $this->createMock(ISecureRandom::class);
$this->remoteWipe = $this->createMock(RemoteWipe::class); $this->remoteWipe = $this->createMock(RemoteWipe::class);
$this->knownUserService = $this->createMock(KnownUserService::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->api = $this->getMockBuilder(UsersController::class) $this->api = $this->getMockBuilder(UsersController::class)
@ -141,6 +145,7 @@ class UsersControllerTest extends TestCase {
$this->federatedShareProviderFactory, $this->federatedShareProviderFactory,
$this->secureRandom, $this->secureRandom,
$this->remoteWipe, $this->remoteWipe,
$this->knownUserService,
$this->eventDispatcher, $this->eventDispatcher,
]) ])
->setMethods(['fillStorageInfo']) ->setMethods(['fillStorageInfo'])
@ -405,6 +410,7 @@ class UsersControllerTest extends TestCase {
$this->federatedShareProviderFactory, $this->federatedShareProviderFactory,
$this->secureRandom, $this->secureRandom,
$this->remoteWipe, $this->remoteWipe,
$this->knownUserService,
$this->eventDispatcher, $this->eventDispatcher,
]) ])
->setMethods(['editUser']) ->setMethods(['editUser'])
@ -1400,6 +1406,13 @@ class UsersControllerTest extends TestCase {
* @param array $expected * @param array $expected
*/ */
public function testSearchByPhoneNumbers(string $location, array $search, int $status, ?array $searchUsers, ?array $userMatches, array $expected) { public function testSearchByPhoneNumbers(string $location, array $search, int $status, ?array $searchUsers, ?array $userMatches, array $expected) {
$knownTo = 'knownTo';
$user = $this->createMock(IUser::class);
$user->method('getUID')
->willReturn($knownTo);
$this->userSession->method('getUser')
->willReturn($user);
if ($searchUsers === null) { if ($searchUsers === null) {
$this->accountManager->expects($this->never()) $this->accountManager->expects($this->never())
->method('searchUsers'); ->method('searchUsers');
@ -1408,6 +1421,14 @@ class UsersControllerTest extends TestCase {
->method('searchUsers') ->method('searchUsers')
->with(IAccountManager::PROPERTY_PHONE, $searchUsers) ->with(IAccountManager::PROPERTY_PHONE, $searchUsers)
->willReturn($userMatches); ->willReturn($userMatches);
$this->knownUserService->expects($this->once())
->method('deleteKnownTo')
->with($knownTo);
$this->knownUserService->expects($this->exactly(count($expected)))
->method('storeIsKnownToUser')
->with($knownTo, $this->anything());
} }
$this->urlGenerator->method('getAbsoluteURL') $this->urlGenerator->method('getAbsoluteURL')
@ -3229,6 +3250,7 @@ class UsersControllerTest extends TestCase {
$this->federatedShareProviderFactory, $this->federatedShareProviderFactory,
$this->secureRandom, $this->secureRandom,
$this->remoteWipe, $this->remoteWipe,
$this->knownUserService,
$this->eventDispatcher, $this->eventDispatcher,
]) ])
->setMethods(['getUserData']) ->setMethods(['getUserData'])
@ -3295,6 +3317,7 @@ class UsersControllerTest extends TestCase {
$this->federatedShareProviderFactory, $this->federatedShareProviderFactory,
$this->secureRandom, $this->secureRandom,
$this->remoteWipe, $this->remoteWipe,
$this->knownUserService,
$this->eventDispatcher, $this->eventDispatcher,
]) ])
->setMethods(['getUserData']) ->setMethods(['getUserData'])

View File

@ -144,6 +144,8 @@ window.addEventListener('DOMContentLoaded', function(){
$('#shareapi_allow_share_dialog_user_enumeration').on('change', function() { $('#shareapi_allow_share_dialog_user_enumeration').on('change', function() {
$('#shareapi_restrict_user_enumeration_to_group_setting').toggleClass('hidden', !this.checked); $('#shareapi_restrict_user_enumeration_to_group_setting').toggleClass('hidden', !this.checked);
$('#shareapi_restrict_user_enumeration_to_phone_setting').toggleClass('hidden', !this.checked);
$('#shareapi_restrict_user_enumeration_combinewarning_setting').toggleClass('hidden', !this.checked);
}) })
$('#allowLinks').change(function() { $('#allowLinks').change(function() {

View File

@ -42,6 +42,7 @@ use OC\AppFramework\Http;
use OC\Encryption\Exceptions\ModuleDoesNotExistsException; use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
use OC\ForbiddenException; use OC\ForbiddenException;
use OC\Group\Manager as GroupManager; use OC\Group\Manager as GroupManager;
use OC\KnownUser\KnownUserService;
use OC\L10N\Factory; use OC\L10N\Factory;
use OC\Security\IdentityProof\Manager; use OC\Security\IdentityProof\Manager;
use OC\User\Manager as UserManager; use OC\User\Manager as UserManager;
@ -96,6 +97,8 @@ class UsersController extends Controller {
private $jobList; private $jobList;
/** @var IManager */ /** @var IManager */
private $encryptionManager; private $encryptionManager;
/** @var KnownUserService */
private $knownUserService;
/** @var IEventDispatcher */ /** @var IEventDispatcher */
private $dispatcher; private $dispatcher;
@ -116,6 +119,7 @@ class UsersController extends Controller {
Manager $keyManager, Manager $keyManager,
IJobList $jobList, IJobList $jobList,
IManager $encryptionManager, IManager $encryptionManager,
KnownUserService $knownUserService,
IEventDispatcher $dispatcher IEventDispatcher $dispatcher
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
@ -132,6 +136,7 @@ class UsersController extends Controller {
$this->keyManager = $keyManager; $this->keyManager = $keyManager;
$this->jobList = $jobList; $this->jobList = $jobList;
$this->encryptionManager = $encryptionManager; $this->encryptionManager = $encryptionManager;
$this->knownUserService = $knownUserService;
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
} }
@ -363,6 +368,19 @@ class UsersController extends Controller {
?string $twitter = null, ?string $twitter = null,
?string $twitterScope = null ?string $twitterScope = null
) { ) {
$user = $this->userSession->getUser();
if (!$user instanceof IUser) {
return new DataResponse(
[
'status' => 'error',
'data' => [
'message' => $this->l10n->t('Invalid user')
]
],
Http::STATUS_UNAUTHORIZED
);
}
$email = strtolower($email); $email = strtolower($email);
if (!empty($email) && !$this->mailer->validateMailAddress($email)) { if (!empty($email) && !$this->mailer->validateMailAddress($email)) {
return new DataResponse( return new DataResponse(
@ -375,8 +393,9 @@ class UsersController extends Controller {
Http::STATUS_UNPROCESSABLE_ENTITY Http::STATUS_UNPROCESSABLE_ENTITY
); );
} }
$user = $this->userSession->getUser();
$data = $this->accountManager->getUser($user); $data = $this->accountManager->getUser($user);
$beforeData = $data;
$data[IAccountManager::PROPERTY_AVATAR] = ['scope' => $avatarScope]; $data[IAccountManager::PROPERTY_AVATAR] = ['scope' => $avatarScope];
if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) { if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
$data[IAccountManager::PROPERTY_DISPLAYNAME] = ['value' => $displayname, 'scope' => $displaynameScope]; $data[IAccountManager::PROPERTY_DISPLAYNAME] = ['value' => $displayname, 'scope' => $displaynameScope];
@ -393,6 +412,9 @@ class UsersController extends Controller {
} }
try { try {
$data = $this->saveUserSettings($user, $data); $data = $this->saveUserSettings($user, $data);
if ($beforeData[IAccountManager::PROPERTY_PHONE]['value'] !== $data[IAccountManager::PROPERTY_PHONE]['value']) {
$this->knownUserService->deleteByContactUserId($user->getUID());
}
return new DataResponse( return new DataResponse(
[ [
'status' => 'success', 'status' => 'success',

View File

@ -73,6 +73,8 @@ class Sharing implements ISettings {
'allowResharing' => $this->config->getAppValue('core', 'shareapi_allow_resharing', 'yes'), 'allowResharing' => $this->config->getAppValue('core', 'shareapi_allow_resharing', 'yes'),
'allowShareDialogUserEnumeration' => $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes'), 'allowShareDialogUserEnumeration' => $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes'),
'restrictUserEnumerationToGroup' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no'), 'restrictUserEnumerationToGroup' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no'),
'restrictUserEnumerationToPhone' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no'),
'restrictUserEnumerationFullMatch' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes'),
'enforceLinkPassword' => Util::isPublicLinkPasswordRequired(), 'enforceLinkPassword' => Util::isPublicLinkPasswordRequired(),
'onlyShareWithGroupMembers' => $this->shareManager->shareWithGroupMembersOnly(), 'onlyShareWithGroupMembers' => $this->shareManager->shareWithGroupMembersOnly(),
'shareAPIEnabled' => $this->config->getAppValue('core', 'shareapi_enabled', 'yes'), 'shareAPIEnabled' => $this->config->getAppValue('core', 'shareapi_enabled', 'yes'),

View File

@ -163,7 +163,7 @@
<?php if ($_['allowShareDialogUserEnumeration'] === 'yes') { <?php if ($_['allowShareDialogUserEnumeration'] === 'yes') {
print_unescaped('checked="checked"'); print_unescaped('checked="checked"');
} ?> /> } ?> />
<label for="shareapi_allow_share_dialog_user_enumeration"><?php p($l->t('Allow username autocompletion in share dialog (if this is disabled the full username or email address needs to be entered)'));?></label><br /> <label for="shareapi_allow_share_dialog_user_enumeration"><?php p($l->t('Allow username autocompletion in share dialog'));?></label><br />
</p> </p>
<p id="shareapi_restrict_user_enumeration_to_group_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no' || $_['allowShareDialogUserEnumeration'] === 'no') { <p id="shareapi_restrict_user_enumeration_to_group_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no' || $_['allowShareDialogUserEnumeration'] === 'no') {
@ -173,7 +173,31 @@
<?php if ($_['restrictUserEnumerationToGroup'] === 'yes') { <?php if ($_['restrictUserEnumerationToGroup'] === 'yes') {
print_unescaped('checked="checked"'); print_unescaped('checked="checked"');
} ?> /> } ?> />
<label for="shareapi_restrict_user_enumeration_to_group"><?php p($l->t('Restrict username autocompletion to users within the same groups'));?></label><br /> <label for="shareapi_restrict_user_enumeration_to_group"><?php p($l->t('Allow username autocompletion to users within the same groups'));?></label><br />
</p>
<p id="shareapi_restrict_user_enumeration_to_phone_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no' || $_['allowShareDialogUserEnumeration'] === 'no') {
p('hidden');
}?>">
<input type="checkbox" name="shareapi_restrict_user_enumeration_to_phone" value="1" id="shareapi_restrict_user_enumeration_to_phone" class="checkbox"
<?php if ($_['restrictUserEnumerationToPhone'] === 'yes') {
print_unescaped('checked="checked"');
} ?> />
<label for="shareapi_restrict_user_enumeration_to_phone"><?php p($l->t('Allow username autocompletion to users based on phonebook matches'));?></label><br />
</p>
<p id="shareapi_restrict_user_enumeration_combinewarning_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no' || $_['allowShareDialogUserEnumeration'] === 'no') {
p('hidden');
}?>">
<em><?php p($l->t('If autocompletion "same group" and "phonebook matches" are enabled a match in either is enough to show the user.'));?></em><br />
</p>
<p id="shareapi_restrict_user_enumeration_full_match_setting" class="indent <?php if ($_['shareAPIEnabled'] === 'no') {
p('hidden');
}?>">
<input type="checkbox" name="shareapi_restrict_user_enumeration_full_match" value="1" id="shareapi_restrict_user_enumeration_full_match" class="checkbox"
<?php if ($_['restrictUserEnumerationFullMatch'] === 'yes') {
print_unescaped('checked="checked"');
} ?> />
<label for="shareapi_restrict_user_enumeration_full_match"><?php p($l->t('Allow username autocompletion when entering the full name or email address (ignoring missing phonebook match and being in the same group)'));?></label><br />
</p> </p>
<p> <p>

View File

@ -32,6 +32,7 @@ namespace OCA\Settings\Tests\Controller;
use OC\Accounts\AccountManager; use OC\Accounts\AccountManager;
use OC\Encryption\Exceptions\ModuleDoesNotExistsException; use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
use OC\Group\Manager; use OC\Group\Manager;
use OC\KnownUser\KnownUserService;
use OCA\Settings\Controller\UsersController; use OCA\Settings\Controller\UsersController;
use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountManager;
use OCP\App\IAppManager; use OCP\App\IAppManager;
@ -91,6 +92,8 @@ class UsersControllerTest extends \Test\TestCase {
private $securityManager; private $securityManager;
/** @var IManager | \PHPUnit\Framework\MockObject\MockObject */ /** @var IManager | \PHPUnit\Framework\MockObject\MockObject */
private $encryptionManager; private $encryptionManager;
/** @var KnownUserService|\PHPUnit\Framework\MockObject\MockObject */
private $knownUserService;
/** @var IEncryptionModule | \PHPUnit\Framework\MockObject\MockObject */ /** @var IEncryptionModule | \PHPUnit\Framework\MockObject\MockObject */
private $encryptionModule; private $encryptionModule;
/** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */ /** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
@ -111,6 +114,7 @@ class UsersControllerTest extends \Test\TestCase {
$this->securityManager = $this->getMockBuilder(\OC\Security\IdentityProof\Manager::class)->disableOriginalConstructor()->getMock(); $this->securityManager = $this->getMockBuilder(\OC\Security\IdentityProof\Manager::class)->disableOriginalConstructor()->getMock();
$this->jobList = $this->createMock(IJobList::class); $this->jobList = $this->createMock(IJobList::class);
$this->encryptionManager = $this->createMock(IManager::class); $this->encryptionManager = $this->createMock(IManager::class);
$this->knownUserService = $this->createMock(KnownUserService::class);
$this->dispatcher = $this->createMock(IEventDispatcher::class); $this->dispatcher = $this->createMock(IEventDispatcher::class);
$this->l->method('t') $this->l->method('t')
@ -147,6 +151,7 @@ class UsersControllerTest extends \Test\TestCase {
$this->securityManager, $this->securityManager,
$this->jobList, $this->jobList,
$this->encryptionManager, $this->encryptionManager,
$this->knownUserService,
$this->dispatcher $this->dispatcher
); );
} else { } else {
@ -168,6 +173,7 @@ class UsersControllerTest extends \Test\TestCase {
$this->securityManager, $this->securityManager,
$this->jobList, $this->jobList,
$this->encryptionManager, $this->encryptionManager,
$this->knownUserService,
$this->dispatcher $this->dispatcher
] ]
)->setMethods($mockedMethods)->getMock(); )->setMethods($mockedMethods)->getMock();

View File

@ -64,95 +64,29 @@ class SharingTest extends TestCase {
public function testGetFormWithoutExcludedGroups() { public function testGetFormWithoutExcludedGroups() {
$this->config $this->config
->expects($this->at(0))
->method('getAppValue') ->method('getAppValue')
->with('core', 'shareapi_exclude_groups_list', '') ->willReturnMap([
->willReturn(''); ['core', 'shareapi_exclude_groups_list', '', ''],
$this->config ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'],
->expects($this->at(1)) ['core', 'shareapi_allow_links', 'yes', 'yes'],
->method('getAppValue') ['core', 'shareapi_allow_public_upload', 'yes', 'yes'],
->with('core', 'shareapi_allow_group_sharing', 'yes') ['core', 'shareapi_allow_resharing', 'yes', 'yes'],
->willReturn('yes'); ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
$this->config ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
->expects($this->at(2)) ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
->method('getAppValue') ['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'yes'],
->with('core', 'shareapi_allow_links', 'yes') ['core', 'shareapi_enabled', 'yes', 'yes'],
->willReturn('yes'); ['core', 'shareapi_default_expire_date', 'no', 'no'],
$this->config ['core', 'shareapi_expire_after_n_days', '7', '7'],
->expects($this->at(3)) ['core', 'shareapi_enforce_expire_date', 'no', 'no'],
->method('getAppValue') ['core', 'shareapi_exclude_groups', 'no', 'no'],
->with('core', 'shareapi_allow_public_upload', 'yes') ['core', 'shareapi_public_link_disclaimertext', null, 'Lorem ipsum'],
->willReturn('yes'); ['core', 'shareapi_enable_link_password_by_default', 'no', 'yes'],
$this->config ['core', 'shareapi_default_permissions', Constants::PERMISSION_ALL, Constants::PERMISSION_ALL],
->expects($this->at(4)) ['core', 'shareapi_default_internal_expire_date', 'no', 'no'],
->method('getAppValue') ['core', 'shareapi_internal_expire_after_n_days', '7', '7'],
->with('core', 'shareapi_allow_resharing', 'yes') ['core', 'shareapi_enforce_internal_expire_date', 'no', 'no'],
->willReturn('yes'); ]);
$this->config
->expects($this->at(5))
->method('getAppValue')
->with('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes')
->willReturn('yes');
$this->config
->expects($this->at(6))
->method('getAppValue')
->with('core', 'shareapi_restrict_user_enumeration_to_group', 'no')
->willReturn('no');
$this->config
->expects($this->at(7))
->method('getAppValue')
->with('core', 'shareapi_enabled', 'yes')
->willReturn('yes');
$this->config
->expects($this->at(8))
->method('getAppValue')
->with('core', 'shareapi_default_expire_date', 'no')
->willReturn('no');
$this->config
->expects($this->at(9))
->method('getAppValue')
->with('core', 'shareapi_expire_after_n_days', '7')
->willReturn('7');
$this->config
->expects($this->at(10))
->method('getAppValue')
->with('core', 'shareapi_enforce_expire_date', 'no')
->willReturn('no');
$this->config
->expects($this->at(11))
->method('getAppValue')
->with('core', 'shareapi_exclude_groups', 'no')
->willReturn('no');
$this->config
->expects($this->at(12))
->method('getAppValue')
->with('core', 'shareapi_public_link_disclaimertext', null)
->willReturn('Lorem ipsum');
$this->config
->expects($this->at(13))
->method('getAppValue')
->with('core', 'shareapi_enable_link_password_by_default', 'no')
->willReturn('yes');
$this->config
->expects($this->at(14))
->method('getAppValue')
->with('core', 'shareapi_default_permissions', Constants::PERMISSION_ALL)
->willReturn(Constants::PERMISSION_ALL);
$this->config
->expects($this->at(15))
->method('getAppValue')
->with('core', 'shareapi_default_internal_expire_date', 'no')
->willReturn('no');
$this->config
->expects($this->at(16))
->method('getAppValue')
->with('core', 'shareapi_internal_expire_after_n_days', '7')
->willReturn('7');
$this->config
->expects($this->at(17))
->method('getAppValue')
->with('core', 'shareapi_enforce_internal_expire_date', 'no')
->willReturn('no');
$expected = new TemplateResponse( $expected = new TemplateResponse(
'settings', 'settings',
@ -164,6 +98,8 @@ class SharingTest extends TestCase {
'allowResharing' => 'yes', 'allowResharing' => 'yes',
'allowShareDialogUserEnumeration' => 'yes', 'allowShareDialogUserEnumeration' => 'yes',
'restrictUserEnumerationToGroup' => 'no', 'restrictUserEnumerationToGroup' => 'no',
'restrictUserEnumerationToPhone' => 'no',
'restrictUserEnumerationFullMatch' => 'yes',
'enforceLinkPassword' => false, 'enforceLinkPassword' => false,
'onlyShareWithGroupMembers' => false, 'onlyShareWithGroupMembers' => false,
'shareAPIEnabled' => 'yes', 'shareAPIEnabled' => 'yes',
@ -188,96 +124,29 @@ class SharingTest extends TestCase {
public function testGetFormWithExcludedGroups() { public function testGetFormWithExcludedGroups() {
$this->config $this->config
->expects($this->at(0))
->method('getAppValue') ->method('getAppValue')
->with('core', 'shareapi_exclude_groups_list', '') ->willReturnMap([
->willReturn('["NoSharers","OtherNoSharers"]'); ['core', 'shareapi_exclude_groups_list', '', '["NoSharers","OtherNoSharers"]'],
$this->config ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'],
->expects($this->at(1)) ['core', 'shareapi_allow_links', 'yes', 'yes'],
->method('getAppValue') ['core', 'shareapi_allow_public_upload', 'yes', 'yes'],
->with('core', 'shareapi_allow_group_sharing', 'yes') ['core', 'shareapi_allow_resharing', 'yes', 'yes'],
->willReturn('yes'); ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
$this->config ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
->expects($this->at(2)) ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
->method('getAppValue') ['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'yes'],
->with('core', 'shareapi_allow_links', 'yes') ['core', 'shareapi_enabled', 'yes', 'yes'],
->willReturn('yes'); ['core', 'shareapi_default_expire_date', 'no', 'no'],
$this->config ['core', 'shareapi_expire_after_n_days', '7', '7'],
->expects($this->at(3)) ['core', 'shareapi_enforce_expire_date', 'no', 'no'],
->method('getAppValue') ['core', 'shareapi_exclude_groups', 'no', 'yes'],
->with('core', 'shareapi_allow_public_upload', 'yes') ['core', 'shareapi_public_link_disclaimertext', null, 'Lorem ipsum'],
->willReturn('yes'); ['core', 'shareapi_enable_link_password_by_default', 'no', 'yes'],
$this->config ['core', 'shareapi_default_permissions', Constants::PERMISSION_ALL, Constants::PERMISSION_ALL],
->expects($this->at(4)) ['core', 'shareapi_default_internal_expire_date', 'no', 'no'],
->method('getAppValue') ['core', 'shareapi_internal_expire_after_n_days', '7', '7'],
->with('core', 'shareapi_allow_resharing', 'yes') ['core', 'shareapi_enforce_internal_expire_date', 'no', 'no'],
->willReturn('yes'); ]);
$this->config
->expects($this->at(5))
->method('getAppValue')
->with('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes')
->willReturn('yes');
$this->config
->expects($this->at(6))
->method('getAppValue')
->with('core', 'shareapi_restrict_user_enumeration_to_group', 'no')
->willReturn('no');
$this->config
->expects($this->at(7))
->method('getAppValue')
->with('core', 'shareapi_enabled', 'yes')
->willReturn('yes');
$this->config
->expects($this->at(8))
->method('getAppValue')
->with('core', 'shareapi_default_expire_date', 'no')
->willReturn('no');
$this->config
->expects($this->at(9))
->method('getAppValue')
->with('core', 'shareapi_expire_after_n_days', '7')
->willReturn('7');
$this->config
->expects($this->at(10))
->method('getAppValue')
->with('core', 'shareapi_enforce_expire_date', 'no')
->willReturn('no');
$this->config
->expects($this->at(11))
->method('getAppValue')
->with('core', 'shareapi_exclude_groups', 'no')
->willReturn('yes');
$this->config
->expects($this->at(12))
->method('getAppValue')
->with('core', 'shareapi_public_link_disclaimertext', null)
->willReturn('Lorem ipsum');
$this->config
->expects($this->at(13))
->method('getAppValue')
->with('core', 'shareapi_enable_link_password_by_default', 'no')
->willReturn('yes');
$this->config
->expects($this->at(14))
->method('getAppValue')
->with('core', 'shareapi_default_permissions', Constants::PERMISSION_ALL)
->willReturn(Constants::PERMISSION_ALL);
$this->config
->expects($this->at(15))
->method('getAppValue')
->with('core', 'shareapi_default_internal_expire_date', 'no')
->willReturn('no');
$this->config
->expects($this->at(16))
->method('getAppValue')
->with('core', 'shareapi_internal_expire_after_n_days', '7')
->willReturn('7');
$this->config
->expects($this->at(17))
->method('getAppValue')
->with('core', 'shareapi_enforce_internal_expire_date', 'no')
->willReturn('no');
$expected = new TemplateResponse( $expected = new TemplateResponse(
'settings', 'settings',
@ -289,6 +158,8 @@ class SharingTest extends TestCase {
'allowResharing' => 'yes', 'allowResharing' => 'yes',
'allowShareDialogUserEnumeration' => 'yes', 'allowShareDialogUserEnumeration' => 'yes',
'restrictUserEnumerationToGroup' => 'no', 'restrictUserEnumerationToGroup' => 'no',
'restrictUserEnumerationToPhone' => 'no',
'restrictUserEnumerationFullMatch' => 'yes',
'enforceLinkPassword' => false, 'enforceLinkPassword' => false,
'onlyShareWithGroupMembers' => false, 'onlyShareWithGroupMembers' => false,
'shareAPIEnabled' => 'yes', 'shareAPIEnabled' => 'yes',

View File

@ -0,0 +1,218 @@
Feature: autocomplete
Background:
Given using api version "2"
And group "commongroup" exists
And user "admin" belongs to group "commongroup"
And user "auto" exists
And user "autocomplete" exists
And user "autocomplete2" exists
And user "autocomplete2" belongs to group "commongroup"
Scenario: getting autocomplete
Given As an "admin"
When get autocomplete for "auto"
| id | source |
| auto | users |
| autocomplete | users |
| autocomplete2 | users |
When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
Then get autocomplete for "auto"
| id | source |
| auto | users |
| autocomplete | users |
| autocomplete2 | users |
Scenario: getting autocomplete without enumeration
Given As an "admin"
When parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no"
Then get autocomplete for "auto"
| id | source |
| auto | users |
Then get autocomplete for "autocomplete"
| id | source |
| autocomplete | users |
When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
Then get autocomplete for "auto"
| id | source |
Then get autocomplete for "autocomplete"
| id | source |
Scenario: getting autocomplete with limited enumeration by group
Given As an "admin"
When parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes"
Then get autocomplete for "auto"
| id | source |
| auto | users |
| autocomplete2 | users |
Then get autocomplete for "autocomplete"
| id | source |
| autocomplete | users |
| autocomplete2 | users |
Then get autocomplete for "autocomplete2"
| id | source |
| autocomplete2 | users |
When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
Then get autocomplete for "autocomplete"
| id | source |
| autocomplete2 | users |
Then get autocomplete for "autocomplete2"
| id | source |
| autocomplete2 | users |
Scenario: getting autocomplete with limited enumeration by phone
Given As an "admin"
When parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
Then get autocomplete for "auto"
| id | source |
| auto | users |
# autocomplete stores their phone number
Given As an "autocomplete"
And sending "PUT" to "/cloud/users/autocomplete" with
| key | phone |
| value | +49 711 / 25 24 28-90 |
And the HTTP status code should be "200"
And the OCS status code should be "200"
Given As an "admin"
Then get autocomplete for "auto"
| id | source |
| auto | users |
# admin populates they have the phone number
When search users by phone for region "DE" with
| random-string1 | 0711 / 252 428-90 |
Then get autocomplete for "auto"
| id | source |
| auto | users |
| autocomplete | users |
When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
Then get autocomplete for "auto"
| id | source |
| autocomplete | users |
Scenario: getting autocomplete with limited enumeration by group or phone
Given As an "admin"
When parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes"
And parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
# autocomplete stores their phone number
Given As an "autocomplete"
And sending "PUT" to "/cloud/users/autocomplete" with
| key | phone |
| value | +49 711 / 25 24 28-90 |
And the HTTP status code should be "200"
And the OCS status code should be "200"
# admin populates they have the phone number
Given As an "admin"
When search users by phone for region "DE" with
| random-string1 | 0711 / 252 428-90 |
Then get autocomplete for "auto"
| id | source |
| auto | users |
| autocomplete | users |
| autocomplete2 | users |
When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
Then get autocomplete for "auto"
| id | source |
| autocomplete | users |
| autocomplete2 | users |
Scenario: getting autocomplete with limited enumeration but sharing is group restricted
Given As an "admin"
When parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes"
And parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
# autocomplete stores their phone number
Given As an "autocomplete"
And sending "PUT" to "/cloud/users/autocomplete" with
| key | phone |
| value | +49 711 / 25 24 28-90 |
And the HTTP status code should be "200"
And the OCS status code should be "200"
# admin populates they have the phone number
Given As an "admin"
When search users by phone for region "DE" with
| random-string1 | 0711 / 252 428-90 |
Then get autocomplete for "auto"
| id | source |
| auto | users |
| autocomplete | users |
| autocomplete2 | users |
When parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes"
Then get autocomplete for "auto"
| id | source |
| autocomplete2 | users |
Scenario: getting autocomplete with limited enumeration by phone but user changes it
Given As an "admin"
When parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
Then get autocomplete for "auto"
| id | source |
| auto | users |
# autocomplete stores their phone number
Given As an "autocomplete"
And sending "PUT" to "/cloud/users/autocomplete" with
| key | phone |
| value | +49 711 / 25 24 28-90 |
And the HTTP status code should be "200"
And the OCS status code should be "200"
Given As an "admin"
Then get autocomplete for "auto"
| id | source |
| auto | users |
# admin populates they have the phone number
When search users by phone for region "DE" with
| random-string1 | 0711 / 252 428-90 |
Then get autocomplete for "auto"
| id | source |
| auto | users |
| autocomplete | users |
# autocomplete changes their phone number
Given As an "autocomplete"
And sending "PUT" to "/cloud/users/autocomplete" with
| key | phone |
| value | +49 711 / 25 24 28-91 |
And the HTTP status code should be "200"
And the OCS status code should be "200"
Given As an "admin"
Then get autocomplete for "auto"
| id | source |
| auto | users |
# admin populates they have the new phone number
When search users by phone for region "DE" with
| random-string1 | 0711 / 252 428-91 |
Then get autocomplete for "auto"
| id | source |
| auto | users |
| autocomplete | users |
Scenario: getting autocomplete without enumeration and sharing is group restricted
Given As an "admin"
When parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no"
And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes"
Then get autocomplete for "auto"
| id | source |
Then get autocomplete for "autocomplete"
| id | source |
Then get autocomplete for "autocomplete2"
| id | source |
| autocomplete2 | users |

View File

@ -45,6 +45,16 @@ default:
- admin - admin
- admin - admin
regular_user_password: 123456 regular_user_password: 123456
collaboration:
paths:
- "%paths.base%/../collaboration_features"
contexts:
- CollaborationContext:
baseUrl: http://localhost:8080/ocs/
admin:
- admin
- admin
regular_user_password: 123456
sharees: sharees:
paths: paths:
- "%paths.base%/../sharees_features" - "%paths.base%/../sharees_features"

View File

@ -202,6 +202,40 @@ trait BasicStructure {
} }
} }
/**
* @param string $verb
* @param string $url
* @param TableNode|array|null $body
* @param array $headers
*/
protected function sendRequestForJSON(string $verb, string $url, $body = null, array $headers = []): void {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url;
$client = new Client();
$options = [];
if ($this->currentUser === 'admin') {
$options['auth'] = ['admin', 'admin'];
} elseif (strpos($this->currentUser, 'guest') !== 0) {
$options['auth'] = [$this->currentUser, self::TEST_PASSWORD];
}
if ($body instanceof TableNode) {
$fd = $body->getRowsHash();
$options['form_params'] = $fd;
} elseif (is_array($body)) {
$options['form_params'] = $body;
}
$options['headers'] = array_merge($headers, [
'OCS-ApiRequest' => 'true',
'Accept' => 'application/json',
]);
try {
$this->response = $client->{$verb}($fullUrl, $options);
} catch (ClientException $ex) {
$this->response = $ex->getResponse();
}
}
/** /**
* @When /^sending "([^"]*)" with exact url to "([^"]*)"$/ * @When /^sending "([^"]*)" with exact url to "([^"]*)"$/
* @param string $verb * @param string $verb

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
require __DIR__ . '/../../vendor/autoload.php';
class CollaborationContext implements Context {
use Provisioning;
use AppConfiguration;
/**
* @Then /^get autocomplete for "([^"]*)"$/
* @param TableNode|null $formData
*/
public function getAutocomplete(string $search, TableNode $formData): void {
$query = $search === 'null' ? null : $search;
$this->sendRequestForJSON('GET', '/core/autocomplete/get?itemType=files&itemId=123&search=' . $query, [
'itemType' => 'files',
'itemId' => '123',
'search' => $query,
]);
$this->theHTTPStatusCodeShouldBe(200);
$data = json_decode($this->response->getBody()->getContents(), true);
$suggestions = $data['ocs']['data'];
Assert::assertCount(count($formData->getHash()), $suggestions, 'Suggestion count does not match');
Assert::assertEquals($formData->getHash(), array_map(static function ($suggestion, $expected) {
$data = [];
if (isset($expected['id'])) {
$data['id'] = $suggestion['id'];
}
if (isset($expected['source'])) {
$data['source'] = $suggestion['source'];
}
return $data;
}, $suggestions, $formData->getHash()));
}
protected function resetAppConfigs(): void {
$this->deleteServerConfig('core', 'shareapi_allow_share_dialog_user_enumeration');
$this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_group');
$this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_phone');
$this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match');
$this->deleteServerConfig('core', 'shareapi_only_share_with_group_members');
}
}

View File

@ -0,0 +1,69 @@
<?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 OC\Core\Migrations;
use Closure;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version21000Date20210309185126 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('known_users')) {
$table = $schema->createTable('known_users');
// Auto increment id
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('known_to', Types::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('known_user', Types::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['known_to'], 'ku_known_to');
return $schema;
}
return null;
}
}

View File

@ -948,6 +948,7 @@ return array(
'OC\\Core\\Migrations\\Version21000Date20201120141228' => $baseDir . '/core/Migrations/Version21000Date20201120141228.php', 'OC\\Core\\Migrations\\Version21000Date20201120141228' => $baseDir . '/core/Migrations/Version21000Date20201120141228.php',
'OC\\Core\\Migrations\\Version21000Date20201202095923' => $baseDir . '/core/Migrations/Version21000Date20201202095923.php', 'OC\\Core\\Migrations\\Version21000Date20201202095923' => $baseDir . '/core/Migrations/Version21000Date20201202095923.php',
'OC\\Core\\Migrations\\Version21000Date20210119195004' => $baseDir . '/core/Migrations/Version21000Date20210119195004.php', 'OC\\Core\\Migrations\\Version21000Date20210119195004' => $baseDir . '/core/Migrations/Version21000Date20210119195004.php',
'OC\\Core\\Migrations\\Version21000Date20210309185126' => $baseDir . '/core/Migrations/Version21000Date20210309185126.php',
'OC\\Core\\Migrations\\Version22000Date20210216080825' => $baseDir . '/core/Migrations/Version22000Date20210216080825.php', 'OC\\Core\\Migrations\\Version22000Date20210216080825' => $baseDir . '/core/Migrations/Version22000Date20210216080825.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
@ -1165,6 +1166,9 @@ return array(
'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => $baseDir . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php', 'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => $baseDir . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php',
'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => $baseDir . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php', 'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => $baseDir . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php',
'OC\\IntegrityCheck\\Iterator\\ExcludeFoldersByPathFilterIterator' => $baseDir . '/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php', 'OC\\IntegrityCheck\\Iterator\\ExcludeFoldersByPathFilterIterator' => $baseDir . '/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php',
'OC\\KnownUser\\KnownUser' => $baseDir . '/lib/private/KnownUser/KnownUser.php',
'OC\\KnownUser\\KnownUserMapper' => $baseDir . '/lib/private/KnownUser/KnownUserMapper.php',
'OC\\KnownUser\\KnownUserService' => $baseDir . '/lib/private/KnownUser/KnownUserService.php',
'OC\\L10N\\Factory' => $baseDir . '/lib/private/L10N/Factory.php', 'OC\\L10N\\Factory' => $baseDir . '/lib/private/L10N/Factory.php',
'OC\\L10N\\L10N' => $baseDir . '/lib/private/L10N/L10N.php', 'OC\\L10N\\L10N' => $baseDir . '/lib/private/L10N/L10N.php',
'OC\\L10N\\L10NString' => $baseDir . '/lib/private/L10N/L10NString.php', 'OC\\L10N\\L10NString' => $baseDir . '/lib/private/L10N/L10NString.php',

View File

@ -977,6 +977,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Migrations\\Version21000Date20201120141228' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20201120141228.php', 'OC\\Core\\Migrations\\Version21000Date20201120141228' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20201120141228.php',
'OC\\Core\\Migrations\\Version21000Date20201202095923' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20201202095923.php', 'OC\\Core\\Migrations\\Version21000Date20201202095923' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20201202095923.php',
'OC\\Core\\Migrations\\Version21000Date20210119195004' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210119195004.php', 'OC\\Core\\Migrations\\Version21000Date20210119195004' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210119195004.php',
'OC\\Core\\Migrations\\Version21000Date20210309185126' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185126.php',
'OC\\Core\\Migrations\\Version22000Date20210216080825' => __DIR__ . '/../../..' . '/core/Migrations/Version22000Date20210216080825.php', 'OC\\Core\\Migrations\\Version22000Date20210216080825' => __DIR__ . '/../../..' . '/core/Migrations/Version22000Date20210216080825.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
@ -1194,6 +1195,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php', 'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php',
'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php', 'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php',
'OC\\IntegrityCheck\\Iterator\\ExcludeFoldersByPathFilterIterator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php', 'OC\\IntegrityCheck\\Iterator\\ExcludeFoldersByPathFilterIterator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php',
'OC\\KnownUser\\KnownUser' => __DIR__ . '/../../..' . '/lib/private/KnownUser/KnownUser.php',
'OC\\KnownUser\\KnownUserMapper' => __DIR__ . '/../../..' . '/lib/private/KnownUser/KnownUserMapper.php',
'OC\\KnownUser\\KnownUserService' => __DIR__ . '/../../..' . '/lib/private/KnownUser/KnownUserService.php',
'OC\\L10N\\Factory' => __DIR__ . '/../../..' . '/lib/private/L10N/Factory.php', 'OC\\L10N\\Factory' => __DIR__ . '/../../..' . '/lib/private/L10N/Factory.php',
'OC\\L10N\\L10N' => __DIR__ . '/../../..' . '/lib/private/L10N/L10N.php', 'OC\\L10N\\L10N' => __DIR__ . '/../../..' . '/lib/private/L10N/L10N.php',
'OC\\L10N\\L10NString' => __DIR__ . '/../../..' . '/lib/private/L10N/L10NString.php', 'OC\\L10N\\L10NString' => __DIR__ . '/../../..' . '/lib/private/L10N/L10NString.php',

View File

@ -27,6 +27,7 @@
namespace OC\Collaboration\Collaborators; namespace OC\Collaboration\Collaborators;
use OC\KnownUser\KnownUserService;
use OCP\Collaboration\Collaborators\ISearchPlugin; use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType; use OCP\Collaboration\Collaborators\SearchResultType;
@ -40,8 +41,16 @@ use OCP\IUserSession;
use OCP\Share\IShare; use OCP\Share\IShare;
class MailPlugin implements ISearchPlugin { class MailPlugin implements ISearchPlugin {
protected $shareeEnumeration; /* @var bool */
protected $shareWithGroupOnly; protected $shareWithGroupOnly;
/* @var bool */
protected $shareeEnumeration;
/* @var bool */
protected $shareeEnumerationInGroupOnly;
/* @var bool */
protected $shareeEnumerationPhone;
/* @var bool */
protected $shareeEnumerationFullMatch;
/** @var IManager */ /** @var IManager */
private $contactsManager; private $contactsManager;
@ -52,20 +61,29 @@ class MailPlugin implements ISearchPlugin {
/** @var IGroupManager */ /** @var IGroupManager */
private $groupManager; private $groupManager;
/** @var KnownUserService */
private $knownUserService;
/** @var IUserSession */ /** @var IUserSession */
private $userSession; private $userSession;
public function __construct(IManager $contactsManager, ICloudIdManager $cloudIdManager, IConfig $config, IGroupManager $groupManager, IUserSession $userSession) { public function __construct(IManager $contactsManager,
ICloudIdManager $cloudIdManager,
IConfig $config,
IGroupManager $groupManager,
KnownUserService $knownUserService,
IUserSession $userSession) {
$this->contactsManager = $contactsManager; $this->contactsManager = $contactsManager;
$this->cloudIdManager = $cloudIdManager; $this->cloudIdManager = $cloudIdManager;
$this->config = $config; $this->config = $config;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->knownUserService = $knownUserService;
$this->userSession = $userSession; $this->userSession = $userSession;
$this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $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->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->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';
} }
/** /**
@ -77,6 +95,8 @@ class MailPlugin implements ISearchPlugin {
* @since 13.0.0 * @since 13.0.0
*/ */
public function search($search, $limit, $offset, ISearchResult $searchResult) { public function search($search, $limit, $offset, ISearchResult $searchResult) {
$currentUserId = $this->userSession->getUser()->getUID();
$result = $userResults = ['wide' => [], 'exact' => []]; $result = $userResults = ['wide' => [], 'exact' => []];
$userType = new SearchResultType('users'); $userType = new SearchResultType('users');
$emailType = new SearchResultType('emails'); $emailType = new SearchResultType('emails');
@ -120,7 +140,7 @@ class MailPlugin implements ISearchPlugin {
continue; continue;
} }
} }
if ($exactEmailMatch) { if ($exactEmailMatch && $this->shareeEnumerationFullMatch) {
try { try {
$cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0]); $cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0]);
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
@ -152,8 +172,12 @@ class MailPlugin implements ISearchPlugin {
continue; continue;
} }
$addToWide = !$this->shareeEnumerationInGroupOnly; $addToWide = !($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone);
if ($this->shareeEnumerationInGroupOnly) { if (!$addToWide && $this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $contact['UID'])) {
$addToWide = true;
}
if (!$addToWide && $this->shareeEnumerationInGroupOnly) {
$addToWide = false; $addToWide = false;
$userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser()); $userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
foreach ($userGroups as $userGroup) { foreach ($userGroups as $userGroup) {
@ -181,7 +205,7 @@ class MailPlugin implements ISearchPlugin {
} }
if ($exactEmailMatch if ($exactEmailMatch
|| isset($contact['FN']) && strtolower($contact['FN']) === $lowerSearch) { || (isset($contact['FN']) && strtolower($contact['FN']) === $lowerSearch)) {
if ($exactEmailMatch) { if ($exactEmailMatch) {
$searchResult->markExactIdMatch($emailType); $searchResult->markExactIdMatch($emailType);
} }

View File

@ -32,6 +32,7 @@
namespace OC\Collaboration\Collaborators; namespace OC\Collaboration\Collaborators;
use OC\KnownUser\KnownUserService;
use OCP\Collaboration\Collaborators\ISearchPlugin; use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType; use OCP\Collaboration\Collaborators\SearchResultType;
@ -46,8 +47,14 @@ use OCP\UserStatus\IManager as IUserStatusManager;
class UserPlugin implements ISearchPlugin { class UserPlugin implements ISearchPlugin {
/* @var bool */ /* @var bool */
protected $shareWithGroupOnly; protected $shareWithGroupOnly;
/* @var bool */
protected $shareeEnumeration; protected $shareeEnumeration;
/* @var bool */
protected $shareeEnumerationInGroupOnly; protected $shareeEnumerationInGroupOnly;
/* @var bool */
protected $shareeEnumerationPhone;
/* @var bool */
protected $shareeEnumerationFullMatch;
/** @var IConfig */ /** @var IConfig */
private $config; private $config;
@ -57,33 +64,30 @@ class UserPlugin implements ISearchPlugin {
private $userSession; private $userSession;
/** @var IUserManager */ /** @var IUserManager */
private $userManager; private $userManager;
/** @var KnownUserService */
private $knownUserService;
/** @var IUserStatusManager */ /** @var IUserStatusManager */
private $userStatusManager; private $userStatusManager;
/**
* UserPlugin constructor.
*
* @param IConfig $config
* @param IUserManager $userManager
* @param IGroupManager $groupManager
* @param IUserSession $userSession
* @param IUserStatusManager $userStatusManager
*/
public function __construct(IConfig $config, public function __construct(IConfig $config,
IUserManager $userManager, IUserManager $userManager,
IGroupManager $groupManager, IGroupManager $groupManager,
IUserSession $userSession, IUserSession $userSession,
KnownUserService $knownUserService,
IUserStatusManager $userStatusManager) { IUserStatusManager $userStatusManager) {
$this->config = $config; $this->config = $config;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->userSession = $userSession; $this->userSession = $userSession;
$this->userManager = $userManager; $this->userManager = $userManager;
$this->knownUserService = $knownUserService;
$this->userStatusManager = $userStatusManager; $this->userStatusManager = $userStatusManager;
$this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $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->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->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';
} }
public function search($search, $limit, $offset, ISearchResult $searchResult) { public function search($search, $limit, $offset, ISearchResult $searchResult) {
@ -91,6 +95,7 @@ class UserPlugin implements ISearchPlugin {
$users = []; $users = [];
$hasMoreResults = false; $hasMoreResults = false;
$currentUserId = $this->userSession->getUser()->getUID();
$currentUserGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser()); $currentUserGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
if ($this->shareWithGroupOnly) { if ($this->shareWithGroupOnly) {
// Search in all the groups this user is part of // Search in all the groups this user is part of
@ -148,6 +153,7 @@ class UserPlugin implements ISearchPlugin {
if ( if (
$this->shareeEnumerationFullMatch &&
$lowerSearch !== '' && (strtolower($uid) === $lowerSearch || $lowerSearch !== '' && (strtolower($uid) === $lowerSearch ||
strtolower($userDisplayName) === $lowerSearch || strtolower($userDisplayName) === $lowerSearch ||
strtolower($userEmail) === $lowerSearch) strtolower($userEmail) === $lowerSearch)
@ -168,11 +174,16 @@ class UserPlugin implements ISearchPlugin {
]; ];
} else { } else {
$addToWideResults = false; $addToWideResults = false;
if ($this->shareeEnumeration && !$this->shareeEnumerationInGroupOnly) { if ($this->shareeEnumeration &&
!($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone)) {
$addToWideResults = true; $addToWideResults = true;
} }
if ($this->shareeEnumerationInGroupOnly) { if ($this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $user->getUID())) {
$addToWideResults = true;
}
if (!$addToWideResults && $this->shareeEnumerationInGroupOnly) {
$commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user)); $commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user));
if (!empty($commonGroups)) { if (!empty($commonGroups)) {
$addToWideResults = true; $addToWideResults = true;
@ -195,7 +206,7 @@ class UserPlugin implements ISearchPlugin {
} }
} }
if ($offset === 0 && !$foundUserById) { if ($this->shareeEnumerationFullMatch && $offset === 0 && !$foundUserById) {
// On page one we try if the search result has a direct hit on the // 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 id and if so, we add that to the exact match list
$user = $this->userManager->get($search); $user = $this->userManager->get($search);

View File

@ -31,6 +31,7 @@
namespace OC\Contacts\ContactsMenu; namespace OC\Contacts\ContactsMenu;
use OC\KnownUser\KnownUserService;
use OCP\Contacts\ContactsMenu\IContactsStore; use OCP\Contacts\ContactsMenu\IContactsStore;
use OCP\Contacts\ContactsMenu\IEntry; use OCP\Contacts\ContactsMenu\IEntry;
use OCP\Contacts\IManager; use OCP\Contacts\IManager;
@ -53,20 +54,19 @@ class ContactsStore implements IContactsStore {
/** @var IGroupManager */ /** @var IGroupManager */
private $groupManager; private $groupManager;
/** /** @var KnownUserService */
* @param IManager $contactsManager private $knownUserService;
* @param IConfig $config
* @param IUserManager $userManager
* @param IGroupManager $groupManager
*/
public function __construct(IManager $contactsManager, public function __construct(IManager $contactsManager,
IConfig $config, IConfig $config,
IUserManager $userManager, IUserManager $userManager,
IGroupManager $groupManager) { IGroupManager $groupManager,
KnownUserService $knownUserService) {
$this->contactsManager = $contactsManager; $this->contactsManager = $contactsManager;
$this->config = $config; $this->config = $config;
$this->userManager = $userManager; $this->userManager = $userManager;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->knownUserService = $knownUserService;
} }
/** /**
@ -103,7 +103,7 @@ class ContactsStore implements IContactsStore {
} }
/** /**
* Filters the contacts. Applies 3 filters: * Filters the contacts. Applied filters:
* 1. filter the current user * 1. filter the current user
* 2. if the `shareapi_allow_share_dialog_user_enumeration` config option is * 2. if the `shareapi_allow_share_dialog_user_enumeration` config option is
* enabled it will filter all local users * enabled it will filter all local users
@ -122,20 +122,22 @@ class ContactsStore implements IContactsStore {
array $entries, array $entries,
$filter) { $filter) {
$disallowEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') !== 'yes'; $disallowEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') !== 'yes';
$restrictEnumeration = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $restrictEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
$restrictEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
$allowEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
$excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes'; $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes';
// whether to filter out local users // whether to filter out local users
$skipLocal = false; $skipLocal = false;
// whether to filter out all users which doesn't have the same group as the current user // whether to filter out all users which don't have a common group as the current user
$ownGroupsOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes' || $restrictEnumeration; $ownGroupsOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
$selfGroups = $this->groupManager->getUserGroupIds($self); $selfGroups = $this->groupManager->getUserGroupIds($self);
if ($excludedGroups) { if ($excludedGroups) {
$excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', ''); $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
$decodedExcludeGroups = json_decode($excludedGroups, true); $decodedExcludeGroups = json_decode($excludedGroups, true);
$excludeGroupsList = ($decodedExcludeGroups !== null) ? $decodedExcludeGroups : []; $excludeGroupsList = $decodedExcludeGroups ?? [];
if (count(array_intersect($excludeGroupsList, $selfGroups)) !== 0) { if (count(array_intersect($excludeGroupsList, $selfGroups)) !== 0) {
// a group of the current user is excluded -> filter all local users // a group of the current user is excluded -> filter all local users
@ -145,47 +147,80 @@ class ContactsStore implements IContactsStore {
$selfUID = $self->getUID(); $selfUID = $self->getUID();
return array_values(array_filter($entries, function (IEntry $entry) use ($self, $skipLocal, $ownGroupsOnly, $selfGroups, $selfUID, $disallowEnumeration, $filter) { return array_values(array_filter($entries, function (IEntry $entry) use ($skipLocal, $ownGroupsOnly, $selfGroups, $selfUID, $disallowEnumeration, $restrictEnumerationGroup, $restrictEnumerationPhone, $allowEnumerationFullMatch, $filter) {
if ($skipLocal && $entry->getProperty('isLocalSystemBook') === true) { if ($entry->getProperty('UID') === $selfUID) {
return false; return false;
} }
if ($entry->getProperty('isLocalSystemBook')) {
if ($skipLocal) {
return false;
}
$checkedCommonGroupAlready = false;
// Prevent enumerating local users // Prevent enumerating local users
if ($disallowEnumeration && $entry->getProperty('isLocalSystemBook')) { if ($disallowEnumeration) {
$filterUser = true; if (!$allowEnumerationFullMatch) {
return false;
}
$filterOutUser = true;
$mailAddresses = $entry->getEMailAddresses(); $mailAddresses = $entry->getEMailAddresses();
foreach ($mailAddresses as $mailAddress) { foreach ($mailAddresses as $mailAddress) {
if ($mailAddress === $filter) { if ($mailAddress === $filter) {
$filterUser = false; $filterOutUser = false;
break; break;
} }
} }
if ($entry->getProperty('UID') && $entry->getProperty('UID') === $filter) { if ($entry->getProperty('UID') && $entry->getProperty('UID') === $filter) {
$filterUser = false; $filterOutUser = false;
} }
if ($filterUser) { if ($filterOutUser) {
return false;
}
} elseif ($restrictEnumerationPhone || $restrictEnumerationGroup) {
$canEnumerate = false;
if ($restrictEnumerationPhone) {
$canEnumerate = $this->knownUserService->isKnownToUser($selfUID, $entry->getProperty('UID'));
}
if (!$canEnumerate && $restrictEnumerationGroup) {
$user = $this->userManager->get($entry->getProperty('UID'));
if ($user === null) {
return false;
}
$contactGroups = $this->groupManager->getUserGroupIds($user);
$canEnumerate = !empty(array_intersect($contactGroups, $selfGroups));
$checkedCommonGroupAlready = true;
}
if (!$canEnumerate) {
return false; return false;
} }
} }
if ($ownGroupsOnly && $entry->getProperty('isLocalSystemBook') === true) { if ($ownGroupsOnly && !$checkedCommonGroupAlready) {
$uid = $this->userManager->get($entry->getProperty('UID')); $user = $this->userManager->get($entry->getProperty('UID'));
if ($uid === null) { if (!$user instanceof IUser) {
return false; return false;
} }
$contactGroups = $this->groupManager->getUserGroupIds($uid); $contactGroups = $this->groupManager->getUserGroupIds($user);
if (count(array_intersect($contactGroups, $selfGroups)) === 0) { if (empty(array_intersect($contactGroups, $selfGroups))) {
// no groups in common, so shouldn't see the contact // no groups in common, so shouldn't see the contact
return false; return false;
} }
} }
}
return $entry->getProperty('UID') !== $selfUID; return true;
})); }));
} }

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 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 OC\KnownUser;
use OCP\AppFramework\Db\Entity;
/**
* @method void setKnownTo(string $knownTo)
* @method string getKnownTo()
* @method void setKnownUser(string $knownUser)
* @method string getKnownUser()
*/
class KnownUser extends Entity {
/** @var string */
protected $knownTo;
/** @var string */
protected $knownUser;
public function __construct() {
$this->addType('knownTo', 'string');
$this->addType('knownUser', 'string');
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 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 OC\KnownUser;
use OCP\AppFramework\Db\QBMapper;
use OCP\IDBConnection;
/**
* @method KnownUser mapRowToEntity(array $row)
*/
class KnownUserMapper extends QBMapper {
/**
* @param IDBConnection $db
*/
public function __construct(IDBConnection $db) {
parent::__construct($db, 'known_users', KnownUser::class);
}
/**
* @param string $knownTo
* @return int Number of deleted entities
*/
public function deleteKnownTo(string $knownTo): int {
$query = $this->db->getQueryBuilder();
$query->delete($this->getTableName())
->where($query->expr()->eq('known_to', $query->createNamedParameter($knownTo)));
return (int) $query->execute();
}
/**
* @param string $knownUser
* @return int Number of deleted entities
*/
public function deleteKnownUser(string $knownUser): int {
$query = $this->db->getQueryBuilder();
$query->delete($this->getTableName())
->where($query->expr()->eq('known_user', $query->createNamedParameter($knownUser)));
return (int) $query->execute();
}
/**
* Returns all "known users" for the given "known to" user
*
* @param string $knownTo
* @return KnownUser[]
*/
public function getKnownUsers(string $knownTo): array {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('known_to', $query->createNamedParameter($knownTo)));
return $this->findEntities($query);
}
public function createKnownUserFromRow(array $row): KnownUser {
return $this->mapRowToEntity([
'id' => $row['s_id'],
'known_to' => $row['known_to'],
'known_user' => $row['known_user'],
]);
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 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 OC\KnownUser;
class KnownUserService {
/** @var KnownUserMapper */
protected $mapper;
/** @var array */
protected $knownUsers = [];
public function __construct(KnownUserMapper $mapper) {
$this->mapper = $mapper;
}
/**
* Delete all matches where the given user is the owner of the phonebook
*
* @param string $knownTo
* @return int Number of deleted matches
*/
public function deleteKnownTo(string $knownTo): int {
return $this->mapper->deleteKnownTo($knownTo);
}
/**
* Delete all matches where the given user is the one in the phonebook
*
* @param string $contactUserId
* @return int Number of deleted matches
*/
public function deleteByContactUserId(string $contactUserId): int {
return $this->mapper->deleteKnownUser($contactUserId);
}
/**
* Store a match because $knownTo has $contactUserId in his phonebook
*
* @param string $knownTo User id of the owner of the phonebook
* @param string $contactUserId User id of the contact in the phonebook
*/
public function storeIsKnownToUser(string $knownTo, string $contactUserId): void {
$entity = new KnownUser();
$entity->setKnownTo($knownTo);
$entity->setKnownUser($contactUserId);
$this->mapper->insert($entity);
}
/**
* Check if $contactUserId is in the phonebook of $knownTo
*
* @param string $knownTo User id of the owner of the phonebook
* @param string $contactUserId User id of the contact in the phonebook
* @return bool
*/
public function isKnownToUser(string $knownTo, string $contactUserId): bool {
if (!isset($this->knownUsers[$knownTo])) {
$entities = $this->mapper->getKnownUsers($knownTo);
$this->knownUsers[$knownTo] = [];
foreach ($entities as $entity) {
$this->knownUsers[$knownTo][$entity->getKnownUser()] = true;
}
}
return isset($this->knownUsers[$knownTo][$contactUserId]);
}
}

View File

@ -1829,6 +1829,15 @@ class Manager implements IManager {
$this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
} }
public function limitEnumerationToPhone(): bool {
return $this->allowEnumeration() &&
$this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
}
public function allowEnumerationFullMatch(): bool {
return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
}
/** /**
* Copied from \OC_Util::isSharingDisabledForUser * Copied from \OC_Util::isSharingDisabledForUser
* *

View File

@ -384,6 +384,22 @@ interface IManager {
*/ */
public function limitEnumerationToGroups(): bool; public function limitEnumerationToGroups(): bool;
/**
* Check if user enumeration is limited to the phonebook matches
*
* @return bool
* @since 21.0.1
*/
public function limitEnumerationToPhone(): bool;
/**
* Check if user enumeration is allowed to return on full match
*
* @return bool
* @since 21.0.1
*/
public function allowEnumerationFullMatch(): bool;
/** /**
* Check if sharing is disabled for the given user * Check if sharing is disabled for the given user
* *

View File

@ -71,16 +71,16 @@ class SettingsContext implements Context, ActorAwareInterface {
// forThe()->checkbox("Restrict username...") can not be used here; that // forThe()->checkbox("Restrict username...") can not be used here; that
// would return the checkbox itself, but the element that the user // would return the checkbox itself, but the element that the user
// interacts with is the label. // interacts with is the label.
return Locator::forThe()->xpath("//label[normalize-space() = 'Restrict username autocompletion to users within the same groups']")-> return Locator::forThe()->xpath("//label[normalize-space() = 'Allow username autocompletion to users within the same groups']")->
describedAs("Restrict username autocompletion to groups checkbox in Sharing section in Administration Sharing Settings"); describedAs("Allow username autocompletion to users within the same groups checkbox in Sharing section in Administration Sharing Settings");
} }
/** /**
* @return Locator * @return Locator
*/ */
public static function restrictUsernameAutocompletionToGroupsCheckboxInput() { public static function restrictUsernameAutocompletionToGroupsCheckboxInput() {
return Locator::forThe()->checkbox("Restrict username autocompletion to users within the same groups")-> return Locator::forThe()->checkbox("Allow username autocompletion to users within the same groups")->
describedAs("Restrict username autocompletion to groups checkbox input in Sharing section in Administration Sharing Settings"); describedAs("Allow username autocompletion to users within the same groups checkbox input in Sharing section in Administration Sharing Settings");
} }
/** /**

View File

@ -26,6 +26,7 @@ namespace Test\Collaboration\Collaborators;
use OC\Collaboration\Collaborators\MailPlugin; use OC\Collaboration\Collaborators\MailPlugin;
use OC\Collaboration\Collaborators\SearchResult; use OC\Collaboration\Collaborators\SearchResult;
use OC\Federation\CloudIdManager; use OC\Federation\CloudIdManager;
use OC\KnownUser\KnownUserService;
use OCP\Collaboration\Collaborators\SearchResultType; use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\Contacts\IManager; use OCP\Contacts\IManager;
use OCP\Federation\ICloudIdManager; use OCP\Federation\ICloudIdManager;
@ -55,6 +56,9 @@ class MailPluginTest extends TestCase {
/** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
protected $groupManager; protected $groupManager;
/** @var KnownUserService|\PHPUnit\Framework\MockObject\MockObject */
protected $knownUserService;
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
protected $userSession; protected $userSession;
@ -64,6 +68,7 @@ class MailPluginTest extends TestCase {
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->contactsManager = $this->createMock(IManager::class); $this->contactsManager = $this->createMock(IManager::class);
$this->groupManager = $this->createMock(IGroupManager::class); $this->groupManager = $this->createMock(IGroupManager::class);
$this->knownUserService = $this->createMock(KnownUserService::class);
$this->userSession = $this->createMock(IUserSession::class); $this->userSession = $this->createMock(IUserSession::class);
$this->cloudIdManager = new CloudIdManager($this->contactsManager); $this->cloudIdManager = new CloudIdManager($this->contactsManager);
@ -71,7 +76,14 @@ class MailPluginTest extends TestCase {
} }
public function instantiatePlugin() { public function instantiatePlugin() {
$this->plugin = new MailPlugin($this->contactsManager, $this->cloudIdManager, $this->config, $this->groupManager, $this->userSession); $this->plugin = new MailPlugin(
$this->contactsManager,
$this->cloudIdManager,
$this->config,
$this->groupManager,
$this->knownUserService,
$this->userSession
);
} }
/** /**

View File

@ -25,6 +25,7 @@ namespace Test\Collaboration\Collaborators;
use OC\Collaboration\Collaborators\SearchResult; use OC\Collaboration\Collaborators\SearchResult;
use OC\Collaboration\Collaborators\UserPlugin; use OC\Collaboration\Collaborators\UserPlugin;
use OC\KnownUser\KnownUserService;
use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\IConfig; use OCP\IConfig;
use OCP\IGroup; use OCP\IGroup;
@ -49,6 +50,9 @@ class UserPluginTest extends TestCase {
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
protected $session; protected $session;
/** @var KnownUserService|\PHPUnit\Framework\MockObject\MockObject */
protected $knownUserService;
/** @var IUserStatusManager|\PHPUnit\Framework\MockObject\MockObject */ /** @var IUserStatusManager|\PHPUnit\Framework\MockObject\MockObject */
protected $userStatusManager; protected $userStatusManager;
@ -78,6 +82,8 @@ class UserPluginTest extends TestCase {
$this->session = $this->createMock(IUserSession::class); $this->session = $this->createMock(IUserSession::class);
$this->knownUserService = $this->createMock(KnownUserService::class);
$this->userStatusManager = $this->createMock(IUserStatusManager::class); $this->userStatusManager = $this->createMock(IUserStatusManager::class);
$this->searchResult = new SearchResult(); $this->searchResult = new SearchResult();
@ -93,6 +99,7 @@ class UserPluginTest extends TestCase {
$this->userManager, $this->userManager,
$this->groupManager, $this->groupManager,
$this->session, $this->session,
$this->knownUserService,
$this->userStatusManager $this->userStatusManager
); );
} }

View File

@ -26,11 +26,13 @@
namespace Tests\Contacts\ContactsMenu; namespace Tests\Contacts\ContactsMenu;
use OC\Contacts\ContactsMenu\ContactsStore; use OC\Contacts\ContactsMenu\ContactsStore;
use OC\KnownUser\KnownUserService;
use OCP\Contacts\IManager; use OCP\Contacts\IManager;
use OCP\IConfig; use OCP\IConfig;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\IUser; use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase; use Test\TestCase;
class ContactsStoreTest extends TestCase { class ContactsStoreTest extends TestCase {
@ -44,6 +46,8 @@ class ContactsStoreTest extends TestCase {
private $groupManager; private $groupManager;
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
private $config; private $config;
/** @var KnownUserService|MockObject */
private $knownUserService;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -52,7 +56,14 @@ class ContactsStoreTest extends TestCase {
$this->userManager = $this->createMock(IUserManager::class); $this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class); $this->groupManager = $this->createMock(IGroupManager::class);
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->contactsStore = new ContactsStore($this->contactsManager, $this->config, $this->userManager, $this->groupManager); $this->knownUserService = $this->createMock(KnownUserService::class);
$this->contactsStore = new ContactsStore(
$this->contactsManager,
$this->config,
$this->userManager,
$this->groupManager,
$this->knownUserService
);
} }
public function testGetContactsWithoutFilter() { public function testGetContactsWithoutFilter() {
@ -171,29 +182,16 @@ class ContactsStoreTest extends TestCase {
} }
public function testGetContactsWhenUserIsInExcludeGroups() { public function testGetContactsWhenUserIsInExcludeGroups() {
$this->config->expects($this->at(0))->method('getAppValue') $this->config
->with($this->equalTo('core'), $this->equalTo('shareapi_allow_share_dialog_user_enumeration'), $this->equalTo('yes'))
->willReturn('yes');
$this->config->expects($this->at(1))
->method('getAppValue') ->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_restrict_user_enumeration_to_group'), $this->equalTo('no')) ->willReturnMap([
->willReturn('no'); ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
$this->config->expects($this->at(2)) ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
->method('getAppValue') ['core', 'shareapi_exclude_groups', 'no', 'yes'],
->with($this->equalTo('core'), $this->equalTo('shareapi_exclude_groups'), $this->equalTo('no')) ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'],
->willReturn('yes'); ['core', 'shareapi_exclude_groups_list', '', '["group1", "group5", "group6"]'],
]);
$this->config->expects($this->at(3))
->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_only_share_with_group_members'), $this->equalTo('no'))
->willReturn('yes');
$this->config->expects($this->at(4))
->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_exclude_groups_list'), $this->equalTo(''))
->willReturn('["group1", "group5", "group6"]');
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */ /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */
$currentUser = $this->createMock(IUser::class); $currentUser = $this->createMock(IUser::class);
@ -228,22 +226,15 @@ class ContactsStoreTest extends TestCase {
} }
public function testGetContactsOnlyShareIfInTheSameGroup() { public function testGetContactsOnlyShareIfInTheSameGroup() {
$this->config->expects($this->at(0))->method('getAppValue') $this->config
->with($this->equalTo('core'), $this->equalTo('shareapi_allow_share_dialog_user_enumeration'), $this->equalTo('yes'))
->willReturn('yes');
$this->config->expects($this->at(1)) ->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_restrict_user_enumeration_to_group'), $this->equalTo('no'))
->willReturn('no');
$this->config->expects($this->at(2)) ->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_exclude_groups'), $this->equalTo('no'))
->willReturn('no');
$this->config->expects($this->at(3))
->method('getAppValue') ->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_only_share_with_group_members'), $this->equalTo('no')) ->willReturnMap([
->willReturn('yes'); ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
['core', 'shareapi_exclude_groups', 'no', 'no'],
['core', 'shareapi_only_share_with_group_members', 'no', 'yes'],
]);
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */ /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */
$currentUser = $this->createMock(IUser::class); $currentUser = $this->createMock(IUser::class);
@ -314,22 +305,15 @@ class ContactsStoreTest extends TestCase {
} }
public function testGetContactsOnlyEnumerateIfInTheSameGroup() { public function testGetContactsOnlyEnumerateIfInTheSameGroup() {
$this->config->expects($this->at(0))->method('getAppValue') $this->config
->with($this->equalTo('core'), $this->equalTo('shareapi_allow_share_dialog_user_enumeration'), $this->equalTo('yes'))
->willReturn('yes');
$this->config->expects($this->at(1)) ->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_restrict_user_enumeration_to_group'), $this->equalTo('no'))
->willReturn('yes');
$this->config->expects($this->at(2)) ->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_exclude_groups'), $this->equalTo('no'))
->willReturn('no');
$this->config->expects($this->at(3))
->method('getAppValue') ->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_only_share_with_group_members'), $this->equalTo('no')) ->willReturnMap([
->willReturn('no'); ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
['core', 'shareapi_exclude_groups', 'no', 'no'],
['core', 'shareapi_only_share_with_group_members', 'no', 'yes'],
]);
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */ /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */
$currentUser = $this->createMock(IUser::class); $currentUser = $this->createMock(IUser::class);
@ -399,10 +383,312 @@ class ContactsStoreTest extends TestCase {
$this->assertEquals('contact', $entries[2]->getProperty('UID')); $this->assertEquals('contact', $entries[2]->getProperty('UID'));
} }
public function testGetContactsOnlyEnumerateIfPhoneBookMatch() {
$this->config
->method('getAppValue')
->willReturnMap([
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'],
['core', 'shareapi_exclude_groups', 'no', 'no'],
['core', 'shareapi_only_share_with_group_members', 'no', 'no'],
]);
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */
$currentUser = $this->createMock(IUser::class);
$currentUser->expects($this->once())
->method('getUID')
->willReturn('user001');
$this->groupManager->expects($this->at(0))
->method('getUserGroupIds')
->with($this->equalTo($currentUser))
->willReturn(['group1', 'group2', 'group3']);
$this->knownUserService->method('isKnownToUser')
->willReturnMap([
['user001', 'user1', true],
['user001', 'user2', true],
['user001', 'user3', false],
]);
$this->contactsManager->expects($this->once())
->method('search')
->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL']))
->willReturn([
[
'UID' => 'user1',
'isLocalSystemBook' => true
],
[
'UID' => 'user2',
'isLocalSystemBook' => true
],
[
'UID' => 'user3',
'isLocalSystemBook' => true
],
[
'UID' => 'contact',
],
]);
$entries = $this->contactsStore->getContacts($currentUser, '');
$this->assertCount(3, $entries);
$this->assertEquals('user1', $entries[0]->getProperty('UID'));
$this->assertEquals('user2', $entries[1]->getProperty('UID'));
$this->assertEquals('contact', $entries[2]->getProperty('UID'));
}
public function testGetContactsOnlyEnumerateIfPhoneBookMatchWithOwnGroupsOnly() {
$this->config
->method('getAppValue')
->willReturnMap([
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'],
['core', 'shareapi_exclude_groups', 'no', 'no'],
['core', 'shareapi_only_share_with_group_members', 'no', 'yes'],
]);
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */
$currentUser = $this->createMock(IUser::class);
$currentUser->expects($this->once())
->method('getUID')
->willReturn('user001');
$this->groupManager->expects($this->at(0))
->method('getUserGroupIds')
->with($this->equalTo($currentUser))
->willReturn(['group1', 'group2', 'group3']);
$user1 = $this->createMock(IUser::class);
$this->userManager->expects($this->at(0))
->method('get')
->with('user1')
->willReturn($user1);
$this->groupManager->expects($this->at(1))
->method('getUserGroupIds')
->with($this->equalTo($user1))
->willReturn(['group1']);
$user2 = $this->createMock(IUser::class);
$this->userManager->expects($this->at(1))
->method('get')
->with('user2')
->willReturn($user2);
$this->groupManager->expects($this->at(2))
->method('getUserGroupIds')
->with($this->equalTo($user2))
->willReturn(['group2', 'group3']);
$user3 = $this->createMock(IUser::class);
$this->userManager->expects($this->at(2))
->method('get')
->with('user3')
->willReturn($user3);
$this->groupManager->expects($this->at(3))
->method('getUserGroupIds')
->with($this->equalTo($user3))
->willReturn(['group8', 'group9']);
$this->knownUserService->method('isKnownToUser')
->willReturnMap([
['user001', 'user1', true],
['user001', 'user2', true],
['user001', 'user3', true],
]);
$this->contactsManager->expects($this->once())
->method('search')
->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL']))
->willReturn([
[
'UID' => 'user1',
'isLocalSystemBook' => true
],
[
'UID' => 'user2',
'isLocalSystemBook' => true
],
[
'UID' => 'user3',
'isLocalSystemBook' => true
],
[
'UID' => 'contact',
],
]);
$entries = $this->contactsStore->getContacts($currentUser, '');
$this->assertCount(3, $entries);
$this->assertEquals('user1', $entries[0]->getProperty('UID'));
$this->assertEquals('user2', $entries[1]->getProperty('UID'));
$this->assertEquals('contact', $entries[2]->getProperty('UID'));
}
public function testGetContactsOnlyEnumerateIfPhoneBookOrSameGroup() {
$this->config
->method('getAppValue')
->willReturnMap([
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'],
['core', 'shareapi_exclude_groups', 'no', 'no'],
['core', 'shareapi_only_share_with_group_members', 'no', 'no'],
]);
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */
$currentUser = $this->createMock(IUser::class);
$currentUser->expects($this->once())
->method('getUID')
->willReturn('user001');
$this->groupManager->expects($this->at(0))
->method('getUserGroupIds')
->with($this->equalTo($currentUser))
->willReturn(['group1', 'group2', 'group3']);
$user1 = $this->createMock(IUser::class);
$this->userManager->expects($this->at(0))
->method('get')
->with('user1')
->willReturn($user1);
$this->groupManager->expects($this->at(1))
->method('getUserGroupIds')
->with($this->equalTo($user1))
->willReturn(['group1']);
$this->knownUserService->method('isKnownToUser')
->willReturnMap([
['user001', 'user1', false],
['user001', 'user2', true],
['user001', 'user3', true],
]);
$this->contactsManager->expects($this->once())
->method('search')
->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL']))
->willReturn([
[
'UID' => 'user1',
'isLocalSystemBook' => true
],
[
'UID' => 'user2',
'isLocalSystemBook' => true
],
[
'UID' => 'user3',
'isLocalSystemBook' => true
],
[
'UID' => 'contact',
],
]);
$entries = $this->contactsStore->getContacts($currentUser, '');
$this->assertCount(4, $entries);
$this->assertEquals('user1', $entries[0]->getProperty('UID'));
$this->assertEquals('user2', $entries[1]->getProperty('UID'));
$this->assertEquals('user3', $entries[2]->getProperty('UID'));
$this->assertEquals('contact', $entries[3]->getProperty('UID'));
}
public function testGetContactsOnlyEnumerateIfPhoneBookOrSameGroupInOwnGroupsOnly() {
$this->config
->method('getAppValue')
->willReturnMap([
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'],
['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'],
['core', 'shareapi_exclude_groups', 'no', 'no'],
['core', 'shareapi_only_share_with_group_members', 'no', 'yes'],
]);
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $currentUser */
$currentUser = $this->createMock(IUser::class);
$currentUser->expects($this->once())
->method('getUID')
->willReturn('user001');
$this->groupManager->expects($this->at(0))
->method('getUserGroupIds')
->with($this->equalTo($currentUser))
->willReturn(['group1', 'group2', 'group3']);
$user1 = $this->createMock(IUser::class);
$this->userManager->expects($this->at(0))
->method('get')
->with('user1')
->willReturn($user1);
$this->groupManager->expects($this->at(1))
->method('getUserGroupIds')
->with($this->equalTo($user1))
->willReturn(['group1']);
$user2 = $this->createMock(IUser::class);
$this->userManager->expects($this->at(1))
->method('get')
->with('user2')
->willReturn($user2);
$this->groupManager->expects($this->at(2))
->method('getUserGroupIds')
->with($this->equalTo($user2))
->willReturn(['group2', 'group3']);
$user3 = $this->createMock(IUser::class);
$this->userManager->expects($this->at(2))
->method('get')
->with('user3')
->willReturn($user3);
$this->groupManager->expects($this->at(3))
->method('getUserGroupIds')
->with($this->equalTo($user3))
->willReturn(['group8', 'group9']);
$this->knownUserService->method('isKnownToUser')
->willReturnMap([
['user001', 'user1', false],
['user001', 'user2', true],
['user001', 'user3', true],
]);
$this->contactsManager->expects($this->once())
->method('search')
->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL']))
->willReturn([
[
'UID' => 'user1',
'isLocalSystemBook' => true
],
[
'UID' => 'user2',
'isLocalSystemBook' => true
],
[
'UID' => 'user3',
'isLocalSystemBook' => true
],
[
'UID' => 'contact',
],
]);
$entries = $this->contactsStore->getContacts($currentUser, '');
$this->assertCount(3, $entries);
$this->assertEquals('user1', $entries[0]->getProperty('UID'));
$this->assertEquals('user2', $entries[1]->getProperty('UID'));
$this->assertEquals('contact', $entries[2]->getProperty('UID'));
}
public function testGetContactsWithFilter() { public function testGetContactsWithFilter() {
$this->config->expects($this->at(0))->method('getAppValue') $this->config
->with($this->equalTo('core'), $this->equalTo('shareapi_allow_share_dialog_user_enumeration'), $this->equalTo('yes')) ->method('getAppValue')
->willReturn('no'); ->willReturnMap([
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'],
['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'yes'],
]);
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */
$user = $this->createMock(IUser::class); $user = $this->createMock(IUser::class);
@ -483,6 +769,90 @@ class ContactsStoreTest extends TestCase {
], $entry[0]->getEMailAddresses()); ], $entry[0]->getEMailAddresses());
} }
public function testGetContactsWithFilterWithoutFullMatch() {
$this->config
->method('getAppValue')
->willReturnMap([
['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'],
['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'no'],
]);
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */
$user = $this->createMock(IUser::class);
$this->contactsManager->expects($this->any())
->method('search')
->willReturn([
[
'UID' => 'a567',
'FN' => 'Darren Roner',
'EMAIL' => [
'darren@roner.au',
],
'isLocalSystemBook' => true,
],
[
'UID' => 'john',
'FN' => 'John Doe',
'EMAIL' => [
'john@example.com',
],
'isLocalSystemBook' => true,
],
[
'FN' => 'Anne D',
'EMAIL' => [
'anne@example.com',
],
'isLocalSystemBook' => false,
],
]);
$user->expects($this->any())
->method('getUID')
->willReturn('user123');
// Complete match on UID should not match
$entry = $this->contactsStore->getContacts($user, 'a567');
$this->assertSame(1, count($entry));
$this->assertEquals([
'anne@example.com'
], $entry[0]->getEMailAddresses());
// Partial match on UID should not match
$entry = $this->contactsStore->getContacts($user, 'a56');
$this->assertSame(1, count($entry));
$this->assertEquals([
'anne@example.com'
], $entry[0]->getEMailAddresses());
// Complete match on email should not match
$entry = $this->contactsStore->getContacts($user, 'john@example.com');
$this->assertSame(1, count($entry));
$this->assertEquals([
'anne@example.com'
], $entry[0]->getEMailAddresses());
// Partial match on email should not match
$entry = $this->contactsStore->getContacts($user, 'john@example.co');
$this->assertSame(1, count($entry));
$this->assertEquals([
'anne@example.com'
], $entry[0]->getEMailAddresses());
// Match on FN should not match
$entry = $this->contactsStore->getContacts($user, 'Darren Roner');
$this->assertSame(1, count($entry));
$this->assertEquals([
'anne@example.com'
], $entry[0]->getEMailAddresses());
// Don't filter users in local addressbook
$entry = $this->contactsStore->getContacts($user, 'Anne D');
$this->assertSame(1, count($entry));
$this->assertEquals([
'anne@example.com'
], $entry[0]->getEMailAddresses());
}
public function testFindOneUser() { public function testFindOneUser() {
$this->config->expects($this->at(0))->method('getAppValue') $this->config->expects($this->at(0))->method('getAppValue')
->with($this->equalTo('core'), $this->equalTo('shareapi_allow_share_dialog_user_enumeration'), $this->equalTo('yes')) ->with($this->equalTo('core'), $this->equalTo('shareapi_allow_share_dialog_user_enumeration'), $this->equalTo('yes'))

View File

@ -30,7 +30,7 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
// when updating major/minor version number. // when updating major/minor version number.
$OC_Version = [22, 0, 0, 1]; $OC_Version = [22, 0, 0, 2];
// The human readable string // The human readable string
$OC_VersionString = '22.0.0 alpha'; $OC_VersionString = '22.0.0 alpha';