diff options
Diffstat (limited to 'lib')
38 files changed, 805 insertions, 149 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 2e6b68f35bb..86d24ad031c 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -534,6 +534,7 @@ return array( 'OCP\\User\\Backend\\IGetRealUIDBackend' => $baseDir . '/lib/public/User/Backend/IGetRealUIDBackend.php', 'OCP\\User\\Backend\\IPasswordConfirmationBackend' => $baseDir . '/lib/public/User/Backend/IPasswordConfirmationBackend.php', 'OCP\\User\\Backend\\IProvideAvatarBackend' => $baseDir . '/lib/public/User/Backend/IProvideAvatarBackend.php', + 'OCP\\User\\Backend\\ISearchKnownUsersBackend' => $baseDir . '/lib/public/User/Backend/ISearchKnownUsersBackend.php', 'OCP\\User\\Backend\\ISetDisplayNameBackend' => $baseDir . '/lib/public/User/Backend/ISetDisplayNameBackend.php', 'OCP\\User\\Backend\\ISetPasswordBackend' => $baseDir . '/lib/public/User/Backend/ISetPasswordBackend.php', 'OCP\\User\\Events\\BeforePasswordUpdatedEvent' => $baseDir . '/lib/public/User/Events/BeforePasswordUpdatedEvent.php', @@ -948,6 +949,8 @@ return array( 'OC\\Core\\Migrations\\Version21000Date20201120141228' => $baseDir . '/core/Migrations/Version21000Date20201120141228.php', 'OC\\Core\\Migrations\\Version21000Date20201202095923' => $baseDir . '/core/Migrations/Version21000Date20201202095923.php', 'OC\\Core\\Migrations\\Version21000Date20210119195004' => $baseDir . '/core/Migrations/Version21000Date20210119195004.php', + 'OC\\Core\\Migrations\\Version21000Date20210309185126' => $baseDir . '/core/Migrations/Version21000Date20210309185126.php', + 'OC\\Core\\Migrations\\Version21000Date20210309185127' => $baseDir . '/core/Migrations/Version21000Date20210309185127.php', 'OC\\Core\\Migrations\\Version22000Date20210216080825' => $baseDir . '/core/Migrations/Version22000Date20210216080825.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', @@ -1165,6 +1168,9 @@ return array( '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\\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\\L10N' => $baseDir . '/lib/private/L10N/L10N.php', 'OC\\L10N\\L10NString' => $baseDir . '/lib/private/L10N/L10NString.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 0a7ebd3ea38..5cf2b9a2cb0 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -563,6 +563,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\User\\Backend\\IGetRealUIDBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/IGetRealUIDBackend.php', 'OCP\\User\\Backend\\IPasswordConfirmationBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/IPasswordConfirmationBackend.php', 'OCP\\User\\Backend\\IProvideAvatarBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/IProvideAvatarBackend.php', + 'OCP\\User\\Backend\\ISearchKnownUsersBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/ISearchKnownUsersBackend.php', 'OCP\\User\\Backend\\ISetDisplayNameBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/ISetDisplayNameBackend.php', 'OCP\\User\\Backend\\ISetPasswordBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/ISetPasswordBackend.php', 'OCP\\User\\Events\\BeforePasswordUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforePasswordUpdatedEvent.php', @@ -977,6 +978,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version21000Date20201120141228' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20201120141228.php', 'OC\\Core\\Migrations\\Version21000Date20201202095923' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20201202095923.php', 'OC\\Core\\Migrations\\Version21000Date20210119195004' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210119195004.php', + 'OC\\Core\\Migrations\\Version21000Date20210309185126' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185126.php', + 'OC\\Core\\Migrations\\Version21000Date20210309185127' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185127.php', 'OC\\Core\\Migrations\\Version22000Date20210216080825' => __DIR__ . '/../../..' . '/core/Migrations/Version22000Date20210216080825.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', @@ -1194,6 +1197,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c '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\\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\\L10N' => __DIR__ . '/../../..' . '/lib/private/L10N/L10N.php', 'OC\\L10N\\L10NString' => __DIR__ . '/../../..' . '/lib/private/L10N/L10NString.php', diff --git a/lib/l10n/zh_HK.js b/lib/l10n/zh_HK.js index ba913a5ab16..7e278f02ad6 100644 --- a/lib/l10n/zh_HK.js +++ b/lib/l10n/zh_HK.js @@ -80,7 +80,7 @@ OC.L10N.register( "Dot files are not allowed" : "不允許小數點開頭的檔案", "Empty filename is not allowed" : "不允許空白的檔名", "App \"%s\" cannot be installed because appinfo file cannot be read." : "應用程式 \"%s\" 無法安裝,因為無法讀取 appinfo 檔案。", - "App \"%s\" cannot be installed because it is not compatible with this version of the server." : "應用程式 \"%s\" 無法安裝,因為該應用程式不相容於目前版本的伺服器。", + "App \"%s\" cannot be installed because it is not compatible with this version of the server." : "應用程式 \"%s\" 無法安裝,因為該應用程式不相容於目前伺服器的版本。", "__language_name__" : "正體中文(香港)", "This is an automatically sent email, please do not reply." : "此為自動寄送的電子郵件,請勿回覆。", "Help" : "說明", @@ -212,7 +212,7 @@ OC.L10N.register( "PHP modules have been installed, but they are still listed as missing?" : "你已經安裝了指定的 PHP 模組,可是還是顯示為找不到嗎?", "Please ask your server administrator to restart the web server." : "請聯絡您的系統管理員重新啟動網頁伺服器", "PostgreSQL >= 9 required" : "需要 PostgreSQL 版本 >= 9", - "Please upgrade your database version" : "請升級您的資料庫版本", + "Please upgrade your database version" : "請升級您數據庫的版本", "Your data directory is readable by other users" : "您的資料目錄可以被其他用戶讀取", "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "請將該目錄權限設定為 0770 ,以免其他用戶讀取目錄列表", "Your data directory must be an absolute path" : "您的資料目錄必須為絕對路徑", diff --git a/lib/l10n/zh_HK.json b/lib/l10n/zh_HK.json index a398d9718de..049293eeb90 100644 --- a/lib/l10n/zh_HK.json +++ b/lib/l10n/zh_HK.json @@ -78,7 +78,7 @@ "Dot files are not allowed" : "不允許小數點開頭的檔案", "Empty filename is not allowed" : "不允許空白的檔名", "App \"%s\" cannot be installed because appinfo file cannot be read." : "應用程式 \"%s\" 無法安裝,因為無法讀取 appinfo 檔案。", - "App \"%s\" cannot be installed because it is not compatible with this version of the server." : "應用程式 \"%s\" 無法安裝,因為該應用程式不相容於目前版本的伺服器。", + "App \"%s\" cannot be installed because it is not compatible with this version of the server." : "應用程式 \"%s\" 無法安裝,因為該應用程式不相容於目前伺服器的版本。", "__language_name__" : "正體中文(香港)", "This is an automatically sent email, please do not reply." : "此為自動寄送的電子郵件,請勿回覆。", "Help" : "說明", @@ -210,7 +210,7 @@ "PHP modules have been installed, but they are still listed as missing?" : "你已經安裝了指定的 PHP 模組,可是還是顯示為找不到嗎?", "Please ask your server administrator to restart the web server." : "請聯絡您的系統管理員重新啟動網頁伺服器", "PostgreSQL >= 9 required" : "需要 PostgreSQL 版本 >= 9", - "Please upgrade your database version" : "請升級您的資料庫版本", + "Please upgrade your database version" : "請升級您數據庫的版本", "Your data directory is readable by other users" : "您的資料目錄可以被其他用戶讀取", "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "請將該目錄權限設定為 0770 ,以免其他用戶讀取目錄列表", "Your data directory must be an absolute path" : "您的資料目錄必須為絕對路徑", diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 05feaf87b8f..c5a0f21319e 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -229,19 +229,23 @@ class AccountManager implements IAccountManager { } public function searchUsers(string $property, array $values): array { + $chunks = array_chunk($values, 500); $query = $this->connection->getQueryBuilder(); $query->select('*') ->from($this->dataTable) ->where($query->expr()->eq('name', $query->createNamedParameter($property))) - ->andWhere($query->expr()->in('value', $query->createNamedParameter($values, IQueryBuilder::PARAM_STR_ARRAY))); + ->andWhere($query->expr()->in('value', $query->createParameter('values'))); - $result = $query->execute(); $matches = []; + foreach ($chunks as $chunk) { + $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY); + $result = $query->execute(); - while ($row = $result->fetch()) { - $matches[$row['value']] = $row['uid']; + while ($row = $result->fetch()) { + $matches[$row['value']] = $row['uid']; + } + $result->closeCursor(); } - $result->closeCursor(); return $matches; } diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php index 4dc517879e8..416e55463cf 100644 --- a/lib/private/App/AppStore/Fetcher/AppFetcher.php +++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php @@ -86,6 +86,10 @@ class AppFetcher extends Fetcher { protected function fetch($ETag, $content, $allowUnstable = false) { /** @var mixed[] $response */ $response = parent::fetch($ETag, $content); + + if (empty($response)) { + return []; + } $allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily'; $allowNightly = $allowUnstable || $this->getChannel() === 'daily'; diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index 2aa5da116e6..84c4c3447a2 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -52,11 +52,11 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { $this->container = new Container(); } - public function get($id) { + public function get(string $id) { return $this->query($id); } - public function has($id): bool { + public function has(string $id): bool { // If a service is no registered but is an existing class, we can probably load it return isset($this->container[$id]) || class_exists($id); } diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php index 7bdd29afc4e..240e16374d5 100644 --- a/lib/private/Collaboration/Collaborators/MailPlugin.php +++ b/lib/private/Collaboration/Collaborators/MailPlugin.php @@ -27,6 +27,7 @@ namespace OC\Collaboration\Collaborators; +use OC\KnownUser\KnownUserService; use OCP\Collaboration\Collaborators\ISearchPlugin; use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\SearchResultType; @@ -40,8 +41,16 @@ use OCP\IUserSession; use OCP\Share\IShare; class MailPlugin implements ISearchPlugin { - protected $shareeEnumeration; + /* @var bool */ protected $shareWithGroupOnly; + /* @var bool */ + protected $shareeEnumeration; + /* @var bool */ + protected $shareeEnumerationInGroupOnly; + /* @var bool */ + protected $shareeEnumerationPhone; + /* @var bool */ + protected $shareeEnumerationFullMatch; /** @var IManager */ private $contactsManager; @@ -52,20 +61,29 @@ class MailPlugin implements ISearchPlugin { /** @var IGroupManager */ private $groupManager; - + /** @var KnownUserService */ + private $knownUserService; /** @var IUserSession */ 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->cloudIdManager = $cloudIdManager; $this->config = $config; $this->groupManager = $groupManager; + $this->knownUserService = $knownUserService; $this->userSession = $userSession; $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + $this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; } /** @@ -77,6 +95,8 @@ class MailPlugin implements ISearchPlugin { * @since 13.0.0 */ public function search($search, $limit, $offset, ISearchResult $searchResult) { + $currentUserId = $this->userSession->getUser()->getUID(); + $result = $userResults = ['wide' => [], 'exact' => []]; $userType = new SearchResultType('users'); $emailType = new SearchResultType('emails'); @@ -120,7 +140,7 @@ class MailPlugin implements ISearchPlugin { continue; } } - if ($exactEmailMatch) { + if ($exactEmailMatch && $this->shareeEnumerationFullMatch) { try { $cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0]); } catch (\InvalidArgumentException $e) { @@ -152,8 +172,12 @@ class MailPlugin implements ISearchPlugin { continue; } - $addToWide = !$this->shareeEnumerationInGroupOnly; - if ($this->shareeEnumerationInGroupOnly) { + $addToWide = !($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone); + if (!$addToWide && $this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $contact['UID'])) { + $addToWide = true; + } + + if (!$addToWide && $this->shareeEnumerationInGroupOnly) { $addToWide = false; $userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser()); foreach ($userGroups as $userGroup) { @@ -181,7 +205,7 @@ class MailPlugin implements ISearchPlugin { } if ($exactEmailMatch - || isset($contact['FN']) && strtolower($contact['FN']) === $lowerSearch) { + || (isset($contact['FN']) && strtolower($contact['FN']) === $lowerSearch)) { if ($exactEmailMatch) { $searchResult->markExactIdMatch($emailType); } diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index d832a42000c..12ed3e98932 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -32,6 +32,7 @@ namespace OC\Collaboration\Collaborators; +use OC\KnownUser\KnownUserService; use OCP\Collaboration\Collaborators\ISearchPlugin; use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\SearchResultType; @@ -46,8 +47,14 @@ use OCP\UserStatus\IManager as IUserStatusManager; class UserPlugin implements ISearchPlugin { /* @var bool */ protected $shareWithGroupOnly; + /* @var bool */ protected $shareeEnumeration; + /* @var bool */ protected $shareeEnumerationInGroupOnly; + /* @var bool */ + protected $shareeEnumerationPhone; + /* @var bool */ + protected $shareeEnumerationFullMatch; /** @var IConfig */ private $config; @@ -57,33 +64,30 @@ class UserPlugin implements ISearchPlugin { private $userSession; /** @var IUserManager */ private $userManager; + /** @var KnownUserService */ + private $knownUserService; /** @var IUserStatusManager */ private $userStatusManager; - /** - * UserPlugin constructor. - * - * @param IConfig $config - * @param IUserManager $userManager - * @param IGroupManager $groupManager - * @param IUserSession $userSession - * @param IUserStatusManager $userStatusManager - */ public function __construct(IConfig $config, IUserManager $userManager, IGroupManager $groupManager, IUserSession $userSession, + KnownUserService $knownUserService, IUserStatusManager $userStatusManager) { $this->config = $config; $this->groupManager = $groupManager; $this->userSession = $userSession; $this->userManager = $userManager; + $this->knownUserService = $knownUserService; $this->userStatusManager = $userStatusManager; $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + $this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; } public function search($search, $limit, $offset, ISearchResult $searchResult) { @@ -91,8 +95,9 @@ class UserPlugin implements ISearchPlugin { $users = []; $hasMoreResults = false; + $currentUserId = $this->userSession->getUser()->getUID(); $currentUserGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser()); - if ($this->shareWithGroupOnly) { + if ($this->shareWithGroupOnly || $this->shareeEnumerationInGroupOnly) { // Search in all the groups this user is part of foreach ($currentUserGroups as $userGroupId) { $usersInGroup = $this->groupManager->displayNamesInGroup($userGroupId, $search, $limit, $offset); @@ -109,9 +114,32 @@ class UserPlugin implements ISearchPlugin { $hasMoreResults = true; } } + + if (!$this->shareWithGroupOnly && $this->shareeEnumerationPhone) { + $usersTmp = $this->userManager->searchKnownUsersByDisplayName($currentUserId, $search, $limit, $offset); + if (!empty($usersTmp)) { + foreach ($usersTmp as $user) { + if ($user->isEnabled()) { // Don't keep deactivated users + $users[$user->getUID()] = $user; + } + } + + uasort($users, function ($a, $b) { + /** + * @var \OC\User\User $a + * @var \OC\User\User $b + */ + return strcasecmp($a->getDisplayName(), $b->getDisplayName()); + }); + } + } } else { // Search in all users - $usersTmp = $this->userManager->searchDisplayName($search, $limit, $offset); + if ($this->shareeEnumerationPhone) { + $usersTmp = $this->userManager->searchKnownUsersByDisplayName($currentUserId, $search, $limit, $offset); + } else { + $usersTmp = $this->userManager->searchDisplayName($search, $limit, $offset); + } foreach ($usersTmp as $user) { if ($user->isEnabled()) { // Don't keep deactivated users $users[$user->getUID()] = $user; @@ -148,6 +176,7 @@ class UserPlugin implements ISearchPlugin { if ( + $this->shareeEnumerationFullMatch && $lowerSearch !== '' && (strtolower($uid) === $lowerSearch || strtolower($userDisplayName) === $lowerSearch || strtolower($userEmail) === $lowerSearch) @@ -168,11 +197,16 @@ class UserPlugin implements ISearchPlugin { ]; } else { $addToWideResults = false; - if ($this->shareeEnumeration && !$this->shareeEnumerationInGroupOnly) { + if ($this->shareeEnumeration && + !($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone)) { + $addToWideResults = true; + } + + if ($this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $user->getUID())) { $addToWideResults = true; } - if ($this->shareeEnumerationInGroupOnly) { + if (!$addToWideResults && $this->shareeEnumerationInGroupOnly) { $commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user)); if (!empty($commonGroups)) { $addToWideResults = true; @@ -195,7 +229,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 // user id and if so, we add that to the exact match list $user = $this->userManager->get($search); diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php index e2bd7edc63d..69f26c7969f 100644 --- a/lib/private/Contacts/ContactsMenu/ContactsStore.php +++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php @@ -31,6 +31,7 @@ namespace OC\Contacts\ContactsMenu; +use OC\KnownUser\KnownUserService; use OCP\Contacts\ContactsMenu\IContactsStore; use OCP\Contacts\ContactsMenu\IEntry; use OCP\Contacts\IManager; @@ -53,20 +54,19 @@ class ContactsStore implements IContactsStore { /** @var IGroupManager */ private $groupManager; - /** - * @param IManager $contactsManager - * @param IConfig $config - * @param IUserManager $userManager - * @param IGroupManager $groupManager - */ + /** @var KnownUserService */ + private $knownUserService; + public function __construct(IManager $contactsManager, IConfig $config, IUserManager $userManager, - IGroupManager $groupManager) { + IGroupManager $groupManager, + KnownUserService $knownUserService) { $this->contactsManager = $contactsManager; $this->config = $config; $this->userManager = $userManager; $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 * 2. if the `shareapi_allow_share_dialog_user_enumeration` config option is * enabled it will filter all local users @@ -122,20 +122,22 @@ class ContactsStore implements IContactsStore { array $entries, $filter) { $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'; // whether to filter out local users $skipLocal = false; - // whether to filter out all users which doesn't have the same group as the current user - $ownGroupsOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes' || $restrictEnumeration; + // 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'; $selfGroups = $this->groupManager->getUserGroupIds($self); if ($excludedGroups) { $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', ''); $decodedExcludeGroups = json_decode($excludedGroups, true); - $excludeGroupsList = ($decodedExcludeGroups !== null) ? $decodedExcludeGroups : []; + $excludeGroupsList = $decodedExcludeGroups ?? []; if (count(array_intersect($excludeGroupsList, $selfGroups)) !== 0) { // a group of the current user is excluded -> filter all local users @@ -145,47 +147,80 @@ class ContactsStore implements IContactsStore { $selfUID = $self->getUID(); - return array_values(array_filter($entries, function (IEntry $entry) use ($self, $skipLocal, $ownGroupsOnly, $selfGroups, $selfUID, $disallowEnumeration, $filter) { - if ($skipLocal && $entry->getProperty('isLocalSystemBook') === true) { + return array_values(array_filter($entries, function (IEntry $entry) use ($skipLocal, $ownGroupsOnly, $selfGroups, $selfUID, $disallowEnumeration, $restrictEnumerationGroup, $restrictEnumerationPhone, $allowEnumerationFullMatch, $filter) { + if ($entry->getProperty('UID') === $selfUID) { return false; } - // Prevent enumerating local users - if ($disallowEnumeration && $entry->getProperty('isLocalSystemBook')) { - $filterUser = true; + if ($entry->getProperty('isLocalSystemBook')) { + if ($skipLocal) { + return false; + } + + $checkedCommonGroupAlready = false; - $mailAddresses = $entry->getEMailAddresses(); - foreach ($mailAddresses as $mailAddress) { - if ($mailAddress === $filter) { - $filterUser = false; - break; + // Prevent enumerating local users + if ($disallowEnumeration) { + if (!$allowEnumerationFullMatch) { + return false; } - } - if ($entry->getProperty('UID') && $entry->getProperty('UID') === $filter) { - $filterUser = false; - } + $filterOutUser = true; - if ($filterUser) { - return false; - } - } + $mailAddresses = $entry->getEMailAddresses(); + foreach ($mailAddresses as $mailAddress) { + if ($mailAddress === $filter) { + $filterOutUser = false; + break; + } + } - if ($ownGroupsOnly && $entry->getProperty('isLocalSystemBook') === true) { - $uid = $this->userManager->get($entry->getProperty('UID')); + if ($entry->getProperty('UID') && $entry->getProperty('UID') === $filter) { + $filterOutUser = false; + } - if ($uid === null) { - return false; + 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; + } } - $contactGroups = $this->groupManager->getUserGroupIds($uid); - if (count(array_intersect($contactGroups, $selfGroups)) === 0) { - // no groups in common, so shouldn't see the contact - return false; + if ($ownGroupsOnly && !$checkedCommonGroupAlready) { + $user = $this->userManager->get($entry->getProperty('UID')); + + if (!$user instanceof IUser) { + return false; + } + + $contactGroups = $this->groupManager->getUserGroupIds($user); + if (empty(array_intersect($contactGroups, $selfGroups))) { + // no groups in common, so shouldn't see the contact + return false; + } } } - return $entry->getProperty('UID') !== $selfUID; + return true; })); } diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 2513abd525f..a18fddcbdeb 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -121,7 +121,7 @@ class Cache implements ICache { $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader); } - private function getQueryBuilder() { + protected function getQueryBuilder() { return new CacheQueryBuilder( $this->connection, \OC::$server->getSystemConfig(), @@ -629,7 +629,7 @@ class Cache implements ICache { /** * Move a file or folder in the cache * - * @param \OCP\Files\Cache\ICache $sourceCache + * @param ICache $sourceCache * @param string $sourcePath * @param string $targetPath * @throws \OC\DatabaseException @@ -1076,4 +1076,47 @@ class Cache implements ICache { public function normalize($path) { return trim(\OC_Util::normalizeUnicode($path), '/'); } + + /** + * Copy a file or folder in the cache + * + * @param ICache $sourceCache + * @param ICacheEntry $sourceEntry + * @param string $targetPath + * @return int fileid of copied entry + */ + public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int { + if ($sourceEntry->getId() < 0) { + throw new \RuntimeException("Invalid source cache entry on copyFromCache"); + } + $data = $this->cacheEntryToArray($sourceEntry); + $fileId = $this->put($targetPath, $data); + if ($fileId <= 0) { + throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " "); + } + if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) { + $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId()); + foreach ($folderContent as $subEntry) { + $subTargetPath = $targetPath . '/' . $subEntry->getName(); + $this->copyFromCache($sourceCache, $subEntry, $subTargetPath); + } + } + return $fileId; + } + + private function cacheEntryToArray(ICacheEntry $entry): array { + return [ + 'size' => $entry->getSize(), + 'mtime' => $entry->getMTime(), + 'storage_mtime' => $entry->getStorageMTime(), + 'mimetype' => $entry->getMimeType(), + 'mimepart' => $entry->getMimePart(), + 'etag' => $entry->getEtag(), + 'permissions' => $entry->getPermissions(), + 'encrypted' => $entry->isEncrypted(), + 'creation_time' => $entry->getCreationTime(), + 'upload_time' => $entry->getUploadTime(), + 'metadata_etag' => $entry->getMetadataEtag(), + ]; + } } diff --git a/lib/private/Files/Cache/FailedCache.php b/lib/private/Files/Cache/FailedCache.php index b57ca2b2572..d9315ec50d5 100644 --- a/lib/private/Files/Cache/FailedCache.php +++ b/lib/private/Files/Cache/FailedCache.php @@ -24,6 +24,7 @@ namespace OC\Files\Cache; use OCP\Constants; use OCP\Files\Cache\ICache; +use OCP\Files\Cache\ICacheEntry; use OCP\Files\Search\ISearchQuery; /** @@ -134,4 +135,8 @@ class FailedCache implements ICache { public function normalize($path) { return $path; } + + public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int { + throw new \Exception("Invalid cache"); + } } diff --git a/lib/private/Files/Cache/MoveFromCacheTrait.php b/lib/private/Files/Cache/MoveFromCacheTrait.php index 0f42b00df1c..2f5bbfb7eea 100644 --- a/lib/private/Files/Cache/MoveFromCacheTrait.php +++ b/lib/private/Files/Cache/MoveFromCacheTrait.php @@ -41,6 +41,8 @@ trait MoveFromCacheTrait { */ abstract public function put($file, array $data); + abstract public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int; + /** * Move a file or folder in the cache * @@ -55,38 +57,4 @@ trait MoveFromCacheTrait { $sourceCache->remove($sourcePath); } - - /** - * Copy a file or folder in the cache - * - * @param \OCP\Files\Cache\ICache $sourceCache - * @param ICacheEntry $sourceEntry - * @param string $targetPath - */ - public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath) { - $this->put($targetPath, $this->cacheEntryToArray($sourceEntry)); - if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) { - $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId()); - foreach ($folderContent as $subEntry) { - $subTargetPath = $targetPath . '/' . $subEntry->getName(); - $this->copyFromCache($sourceCache, $subEntry, $subTargetPath); - } - } - } - - private function cacheEntryToArray(ICacheEntry $entry) { - return [ - 'size' => $entry->getSize(), - 'mtime' => $entry->getMTime(), - 'storage_mtime' => $entry->getStorageMTime(), - 'mimetype' => $entry->getMimeType(), - 'mimepart' => $entry->getMimePart(), - 'etag' => $entry->getEtag(), - 'permissions' => $entry->getPermissions(), - 'encrypted' => $entry->isEncrypted(), - 'creation_time' => $entry->getCreationTime(), - 'upload_time' => $entry->getUploadTime(), - 'metadata_etag' => $entry->getMetadataEtag(), - ]; - } } diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index da1f5729d3c..fc4b582cbce 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -166,6 +166,9 @@ class QuerySearchHelper { $field = 'tag.category'; } elseif ($field === 'fileid') { $field = 'file.fileid'; + } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL) { + $field = 'path_hash'; + $value = md5((string)$value); } return [$field, $value, $type]; } @@ -175,6 +178,7 @@ class QuerySearchHelper { 'mimetype' => 'string', 'mtime' => 'integer', 'name' => 'string', + 'path' => 'string', 'size' => 'integer', 'tagname' => 'string', 'favorite' => 'boolean', @@ -184,6 +188,7 @@ class QuerySearchHelper { 'mimetype' => ['eq', 'like'], 'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'], 'name' => ['eq', 'like'], + 'path' => ['eq', 'like'], 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'], 'tagname' => ['eq', 'like'], 'favorite' => ['eq'], diff --git a/lib/private/Files/Cache/Wrapper/CacheJail.php b/lib/private/Files/Cache/Wrapper/CacheJail.php index 6c1c17be028..fe3124301ba 100644 --- a/lib/private/Files/Cache/Wrapper/CacheJail.php +++ b/lib/private/Files/Cache/Wrapper/CacheJail.php @@ -29,8 +29,13 @@ namespace OC\Files\Cache\Wrapper; use OC\Files\Cache\Cache; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchQuery; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchQuery; /** @@ -41,6 +46,7 @@ class CacheJail extends CacheWrapper { * @var string */ protected $root; + protected $unjailedRoot; /** * @param \OCP\Files\Cache\ICache $cache @@ -49,12 +55,29 @@ class CacheJail extends CacheWrapper { public function __construct($cache, $root) { parent::__construct($cache); $this->root = $root; + $this->connection = \OC::$server->getDatabaseConnection(); + $this->mimetypeLoader = \OC::$server->getMimeTypeLoader(); + + if ($cache instanceof CacheJail) { + $this->unjailedRoot = $cache->getSourcePath($root); + } else { + $this->unjailedRoot = $root; + } } protected function getRoot() { return $this->root; } + /** + * Get the root path with any nested jails resolved + * + * @return string + */ + protected function getGetUnjailedRoot() { + return $this->unjailedRoot; + } + protected function getSourcePath($path) { if ($path === '') { return $this->getRoot(); @@ -65,16 +88,20 @@ class CacheJail extends CacheWrapper { /** * @param string $path + * @param null|string $root * @return null|string the jailed path or null if the path is outside the jail */ - protected function getJailedPath($path) { - if ($this->getRoot() === '') { + protected function getJailedPath(string $path, string $root = null) { + if ($root === null) { + $root = $this->getRoot(); + } + if ($root === '') { return $path; } - $rootLength = strlen($this->getRoot()) + 1; - if ($path === $this->getRoot()) { + $rootLength = strlen($root) + 1; + if ($path === $root) { return ''; - } elseif (substr($path, 0, $rootLength) === $this->getRoot() . '/') { + } elseif (substr($path, 0, $rootLength) === $root . '/') { return substr($path, $rootLength); } else { return null; @@ -92,11 +119,6 @@ class CacheJail extends CacheWrapper { return $entry; } - protected function filterCacheEntry($entry) { - $rootLength = strlen($this->getRoot()) + 1; - return $rootLength === 1 || ($entry['path'] === $this->getRoot()) || (substr($entry['path'], 0, $rootLength) === $this->getRoot() . '/'); - } - /** * get the stored metadata of a file or folder * @@ -209,9 +231,10 @@ class CacheJail extends CacheWrapper { } private function formatSearchResults($results) { - $results = array_filter($results, [$this, 'filterCacheEntry']); - $results = array_values($results); - return array_map([$this, 'formatCacheEntry'], $results); + return array_map(function ($entry) { + $entry['path'] = $this->getJailedPath($entry['path'], $this->getGetUnjailedRoot()); + return $entry; + }, $results); } /** @@ -221,7 +244,29 @@ class CacheJail extends CacheWrapper { * @return array an array of file data */ public function search($pattern) { - $results = $this->getCache()->search($pattern); + // normalize pattern + $pattern = $this->normalize($pattern); + + if ($pattern === '%%') { + return []; + } + + $query = $this->getQueryBuilder(); + $query->selectFileCache() + ->whereStorageId() + ->andWhere($query->expr()->orX( + $query->expr()->like('path', $query->createNamedParameter($this->getGetUnjailedRoot() . '/%')), + $query->expr()->eq('path_hash', $query->createNamedParameter(md5($this->getGetUnjailedRoot()))), + )) + ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern))); + + $result = $query->execute(); + $files = $result->fetchAll(); + $result->closeCursor(); + + $results = array_map(function (array $data) { + return self::cacheEntryFromData($data, $this->mimetypeLoader); + }, $files); return $this->formatSearchResults($results); } @@ -232,12 +277,48 @@ class CacheJail extends CacheWrapper { * @return array */ public function searchByMime($mimetype) { - $results = $this->getCache()->searchByMime($mimetype); + $mimeId = $this->mimetypeLoader->getId($mimetype); + + $query = $this->getQueryBuilder(); + $query->selectFileCache() + ->whereStorageId() + ->andWhere($query->expr()->orX( + $query->expr()->like('path', $query->createNamedParameter($this->getGetUnjailedRoot() . '/%')), + $query->expr()->eq('path_hash', $query->createNamedParameter(md5($this->getGetUnjailedRoot()))), + )); + + if (strpos($mimetype, '/')) { + $query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT))); + } else { + $query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT))); + } + + $result = $query->execute(); + $files = $result->fetchAll(); + $result->closeCursor(); + + $results = array_map(function (array $data) { + return self::cacheEntryFromData($data, $this->mimetypeLoader); + }, $files); return $this->formatSearchResults($results); } public function searchQuery(ISearchQuery $query) { - $simpleQuery = new SearchQuery($query->getSearchOperation(), 0, 0, $query->getOrder(), $query->getUser()); + $prefixFilter = new SearchComparison( + ISearchComparison::COMPARE_LIKE, + 'path', + $this->getGetUnjailedRoot() . '/%' + ); + $rootFilter = new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'path', + $this->getGetUnjailedRoot() + ); + $operation = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_AND, + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [$prefixFilter, $rootFilter]) , $query->getSearchOperation()] + ); + $simpleQuery = new SearchQuery($operation, 0, 0, $query->getOrder(), $query->getUser()); $results = $this->getCache()->searchQuery($simpleQuery); $results = $this->formatSearchResults($results); diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index 3378f00c4dd..5d2cbe61ab6 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -33,12 +33,14 @@ namespace OC\Files\ObjectStore; use Icewind\Streams\CallbackWrapper; use Icewind\Streams\CountWrapper; use Icewind\Streams\IteratorDirectory; +use OC\Files\Cache\Cache; use OC\Files\Cache\CacheEntry; use OC\Files\Storage\PolyFill\CopyDirectory; use OCP\Files\Cache\ICacheEntry; use OCP\Files\FileInfo; use OCP\Files\NotFoundException; use OCP\Files\ObjectStore\IObjectStore; +use OCP\Files\Storage\IStorage; class ObjectStoreStorage extends \OC\Files\Storage\Common { use CopyDirectory; @@ -530,6 +532,19 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { return $this->objectStore; } + public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) { + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) { + $sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath); + $this->copyInner($sourceEntry, $targetInternalPath); + return true; + } + } + + return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + } + public function copy($path1, $path2) { $path1 = $this->normalizePath($path1); $path2 = $this->normalizePath($path2); @@ -567,14 +582,13 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { $sourceUrn = $this->getURN($sourceEntry->getId()); - $cache->copyFromCache($cache, $sourceEntry, $to); - $targetEntry = $cache->get($to); - - if (!$targetEntry) { - throw new \Exception('Target not in cache after copy'); + if (!$cache instanceof Cache) { + throw new \Exception("Invalid source cache for object store copy"); } - $targetUrn = $this->getURN($targetEntry->getId()); + $targetId = $cache->copyFromCache($cache, $sourceEntry, $to); + + $targetUrn = $this->getURN($targetId); try { $this->objectStore->copyObject($sourceUrn, $targetUrn); diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index 80b8a6f132d..4d6ac3608df 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -104,8 +104,6 @@ trait S3ObjectTrait { throw $e; } } - - fclose($countStream); } /** diff --git a/lib/private/Files/SimpleFS/SimpleFile.php b/lib/private/Files/SimpleFS/SimpleFile.php index 108f53ee9c7..0d3cbc7b743 100644 --- a/lib/private/Files/SimpleFS/SimpleFile.php +++ b/lib/private/Files/SimpleFS/SimpleFile.php @@ -180,4 +180,8 @@ class SimpleFile implements ISimpleFile { public function write() { return $this->file->fopen('w'); } + + public function getId(): int { + return $this->file->getId(); + } } diff --git a/lib/private/Files/Storage/Wrapper/Jail.php b/lib/private/Files/Storage/Wrapper/Jail.php index b7933708da8..276f00af33c 100644 --- a/lib/private/Files/Storage/Wrapper/Jail.php +++ b/lib/private/Files/Storage/Wrapper/Jail.php @@ -48,7 +48,7 @@ class Jail extends Wrapper { protected $rootPath; /** - * @param array $arguments ['storage' => $storage, 'mask' => $root] + * @param array $arguments ['storage' => $storage, 'root' => $root] * * $storage: The storage that will be wrapper * $root: The folder in the wrapped storage that will become the root folder of the wrapped storage diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index ed7d6c26318..2223858b7a9 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1643,6 +1643,7 @@ class View { $mount = $this->getMount(''); $mountPoint = $mount->getMountPoint(); $storage = $mount->getStorage(); + $userManager = \OC::$server->getUserManager(); if ($storage) { $cache = $storage->getCache(''); @@ -1652,7 +1653,7 @@ class View { $internalPath = $result['path']; $path = $mountPoint . $result['path']; $result['path'] = substr($mountPoint . $result['path'], $rootLength); - $owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath)); + $owner = $userManager->get($storage->getOwner($internalPath)); $files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner); } } @@ -1671,7 +1672,7 @@ class View { $internalPath = $result['path']; $result['path'] = rtrim($relativeMountPoint . $result['path'], '/'); $path = rtrim($mountPoint . $internalPath, '/'); - $owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath)); + $owner = $userManager->get($storage->getOwner($internalPath)); $files[] = new FileInfo($path, $storage, $internalPath, $result, $mount, $owner); } } diff --git a/lib/private/InitialStateService.php b/lib/private/InitialStateService.php index bbab831b915..bbc893269f7 100644 --- a/lib/private/InitialStateService.php +++ b/lib/private/InitialStateService.php @@ -69,7 +69,7 @@ class InitialStateService implements IInitialStateService { return; } - $this->logger->warning('Invalid data provided to provideInitialState by ' . $appName); + $this->logger->warning('Invalid '. $key . ' data provided to provideInitialState by ' . $appName); } public function provideLazyInitialState(string $appName, string $key, Closure $closure): void { diff --git a/lib/private/KnownUser/KnownUser.php b/lib/private/KnownUser/KnownUser.php new file mode 100644 index 00000000000..939c9199c79 --- /dev/null +++ b/lib/private/KnownUser/KnownUser.php @@ -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'); + } +} diff --git a/lib/private/KnownUser/KnownUserMapper.php b/lib/private/KnownUser/KnownUserMapper.php new file mode 100644 index 00000000000..e77e4752702 --- /dev/null +++ b/lib/private/KnownUser/KnownUserMapper.php @@ -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'], + ]); + } +} diff --git a/lib/private/KnownUser/KnownUserService.php b/lib/private/KnownUser/KnownUserService.php new file mode 100644 index 00000000000..96af21c836f --- /dev/null +++ b/lib/private/KnownUser/KnownUserService.php @@ -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]); + } +} diff --git a/lib/private/Lockdown/Filesystem/NullCache.php b/lib/private/Lockdown/Filesystem/NullCache.php index 396bf68d5df..267b76ab8fb 100644 --- a/lib/private/Lockdown/Filesystem/NullCache.php +++ b/lib/private/Lockdown/Filesystem/NullCache.php @@ -26,6 +26,7 @@ namespace OC\Lockdown\Filesystem; use OC\Files\Cache\CacheEntry; use OCP\Constants; use OCP\Files\Cache\ICache; +use OCP\Files\Cache\ICacheEntry; use OCP\Files\FileInfo; use OCP\Files\Search\ISearchQuery; @@ -122,4 +123,8 @@ class NullCache implements ICache { public function normalize($path) { return $path; } + + public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int { + throw new \OC\ForbiddenException('This request is not allowed to access the filesystem'); + } } diff --git a/lib/private/Search/Provider/File.php b/lib/private/Search/Provider/File.php index b4e35d374ca..4125b1f7d70 100644 --- a/lib/private/Search/Provider/File.php +++ b/lib/private/Search/Provider/File.php @@ -30,23 +30,33 @@ namespace OC\Search\Provider; use OC\Files\Filesystem; +use OCP\Search\PagedProvider; /** * Provide search results from the 'files' app * @deprecated 20.0.0 */ -class File extends \OCP\Search\Provider { +class File extends PagedProvider { /** * Search for files and folders matching the given query + * * @param string $query + * @param int|null $limit + * @param int|null $offset * @return \OCP\Search\Result[] * @deprecated 20.0.0 */ - public function search($query) { + public function search($query, int $limit = null, int $offset = null) { + if ($offset === null) { + $offset = 0; + } \OC_Util::setupFS(); $files = Filesystem::search($query); $results = []; + if ($limit !== null) { + $files = array_slice($files, $offset, $offset + $limit); + } // edit results foreach ($files as $fileData) { // skip versions @@ -79,4 +89,12 @@ class File extends \OCP\Search\Provider { // return return $results; } + + public function searchPaged($query, $page, $size) { + if ($size === 0) { + return $this->search($query); + } else { + return $this->search($query, $size, ($page - 1) * $size); + } + } } diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 5c8dba5915a..ce1ec1d60f6 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -1829,6 +1829,15 @@ class Manager implements IManager { $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 * diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php index 69f36edf4f3..87f77701ee2 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -30,8 +30,9 @@ namespace OC\Share20; -use OCP\Files\Cache\ICacheEntry; use OCP\Files\File; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\FileInfo; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotFoundException; @@ -233,8 +234,13 @@ class Share implements \OCP\Share\IShare { */ public function getNodeType() { if ($this->nodeType === null) { - $node = $this->getNode(); - $this->nodeType = $node instanceof File ? 'file' : 'folder'; + if ($this->getNodeCacheEntry()) { + $info = $this->getNodeCacheEntry(); + $this->nodeType = $info->getMimeType() === FileInfo::MIMETYPE_FOLDER ? 'folder' : 'file'; + } else { + $node = $this->getNode(); + $this->nodeType = $node instanceof File ? 'file' : 'folder'; + } } return $this->nodeType; diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index f0de31568f4..6c68f5d805f 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -59,6 +59,8 @@ class URLGenerator implements IURLGenerator { private $request; /** @var Router */ private $router; + /** @var null|string */ + private $baseUrl = null; public function __construct(IConfig $config, ICacheFactory $cacheFactory, @@ -269,6 +271,9 @@ class URLGenerator implements IURLGenerator { * @return string base url of the current request */ public function getBaseUrl(): string { - return $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT; + if ($this->baseUrl === null) { + $this->baseUrl = $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT; + } + return $this->baseUrl; } } diff --git a/lib/private/User/Backend.php b/lib/private/User/Backend.php index c87dc5d2d50..d70d13673cb 100644 --- a/lib/private/User/Backend.php +++ b/lib/private/User/Backend.php @@ -143,8 +143,8 @@ abstract class Backend implements UserInterface { * Get a list of all display names and user ids. * * @param string $search - * @param string|null $limit - * @param string|null $offset + * @param int|null $limit + * @param int|null $offset * @return array an array of all displayNames (value) and the corresponding uids (key) */ public function getDisplayNames($search = '', $limit = null, $offset = null) { diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php index 85e22d196e4..6c04a1b900e 100644 --- a/lib/private/User/Database.php +++ b/lib/private/User/Database.php @@ -70,6 +70,7 @@ use OCP\User\Backend\ICreateUserBackend; use OCP\User\Backend\IGetDisplayNameBackend; use OCP\User\Backend\IGetHomeBackend; use OCP\User\Backend\IGetRealUIDBackend; +use OCP\User\Backend\ISearchKnownUsersBackend; use OCP\User\Backend\ISetDisplayNameBackend; use OCP\User\Backend\ISetPasswordBackend; @@ -84,6 +85,7 @@ class Database extends ABackend implements ICheckPasswordBackend, IGetHomeBackend, ICountUsersBackend, + ISearchKnownUsersBackend, IGetRealUIDBackend { /** @var CappedMemoryCache */ private $cache; @@ -254,8 +256,8 @@ class Database extends ABackend implements * Get a list of all display names and user ids. * * @param string $search - * @param string|null $limit - * @param string|null $offset + * @param int|null $limit + * @param int|null $offset * @return array an array of all displayNames (value) and the corresponding uids (key) */ public function getDisplayNames($search = '', $limit = null, $offset = null) { @@ -277,7 +279,47 @@ class Database extends ABackend implements ->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%'))) ->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%'))) ->orderBy($query->func()->lower('displayname'), 'ASC') - ->orderBy('uid_lower', 'ASC') + ->addOrderBy('uid_lower', 'ASC') + ->setMaxResults($limit) + ->setFirstResult($offset); + + $result = $query->execute(); + $displayNames = []; + while ($row = $result->fetch()) { + $displayNames[(string)$row['uid']] = (string)$row['displayname']; + } + + return $displayNames; + } + + /** + * @param string $searcher + * @param string $pattern + * @param int|null $limit + * @param int|null $offset + * @return array + * @since 21.0.1 + */ + public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array { + $limit = $this->fixLimit($limit); + + $this->fixDI(); + + $query = $this->dbConn->getQueryBuilder(); + + $query->select('u.uid', 'u.displayname') + ->from($this->table, 'u') + ->leftJoin('u', 'known_users', 'k', $query->expr()->andX( + $query->expr()->eq('k.known_user', 'u.uid'), + $query->expr()->eq('k.known_to', $query->createNamedParameter($searcher)) + )) + ->where($query->expr()->eq('k.known_to', $query->createNamedParameter($searcher))) + ->andWhere($query->expr()->orX( + $query->expr()->iLike('u.uid', $query->createNamedParameter('%' . $this->dbConn->escapeLikeParameter($pattern) . '%')), + $query->expr()->iLike('u.displayname', $query->createNamedParameter('%' . $this->dbConn->escapeLikeParameter($pattern) . '%')) + )) + ->orderBy('u.displayname', 'ASC') + ->addOrderBy('u.uid_lower', 'ASC') ->setMaxResults($limit) ->setFirstResult($offset); diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 036d2703d35..59c007b6b59 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -47,6 +47,7 @@ use OCP\IUserBackend; use OCP\IUserManager; use OCP\Support\Subscription\IRegistry; use OCP\User\Backend\IGetRealUIDBackend; +use OCP\User\Backend\ISearchKnownUsersBackend; use OCP\User\Events\BeforeUserCreatedEvent; use OCP\User\Events\UserCreatedEvent; use OCP\UserInterface; @@ -330,6 +331,41 @@ class Manager extends PublicEmitter implements IUserManager { } /** + * Search known users (from phonebook sync) by displayName + * + * @param string $searcher + * @param string $pattern + * @param int|null $limit + * @param int|null $offset + * @return IUser[] + */ + public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array { + $users = []; + foreach ($this->backends as $backend) { + if ($backend instanceof ISearchKnownUsersBackend) { + $backendUsers = $backend->searchKnownUsersByDisplayName($searcher, $pattern, $limit, $offset); + } else { + // Better than nothing, but filtering after pagination can remove lots of results. + $backendUsers = $backend->getDisplayNames($pattern, $limit, $offset); + } + if (is_array($backendUsers)) { + foreach ($backendUsers as $uid => $displayName) { + $users[] = $this->getUserObject($uid, $backend); + } + } + } + + usort($users, function ($a, $b) { + /** + * @var IUser $a + * @var IUser $b + */ + return strcasecmp($a->getDisplayName(), $b->getDisplayName()); + }); + return $users; + } + + /** * @param string $uid * @param string $password * @throws \InvalidArgumentException @@ -681,6 +717,7 @@ class Manager extends PublicEmitter implements IUserManager { if (\in_array($uid, [ '.htaccess', 'files_external', + '__groupfolders', '.ocdata', 'owncloud.log', 'nextcloud.log', diff --git a/lib/public/Files/Cache/ICache.php b/lib/public/Files/Cache/ICache.php index 95ca29c2aa8..323359dcf68 100644 --- a/lib/public/Files/Cache/ICache.php +++ b/lib/public/Files/Cache/ICache.php @@ -180,6 +180,17 @@ interface ICache { public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath); /** + * Copy a file or folder in the cache + * + * @param ICache $sourceCache + * @param ICacheEntry $sourceEntry + * @param string $targetPath + * @return int fileid of copied entry + * @since 22.0.0 + */ + public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int; + + /** * Get the scan status of a file * * - ICache::NOT_FOUND: File is not in the cache diff --git a/lib/public/IUserManager.php b/lib/public/IUserManager.php index e8a7fc7827d..6963bb5ddbc 100644 --- a/lib/public/IUserManager.php +++ b/lib/public/IUserManager.php @@ -127,6 +127,18 @@ interface IUserManager { public function searchDisplayName($pattern, $limit = null, $offset = null); /** + * Search known users (from phonebook sync) by displayName + * + * @param string $searcher + * @param string $pattern + * @param int|null $limit + * @param int|null $offset + * @return IUser[] + * @since 21.0.1 + */ + public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array; + + /** * @param string $uid * @param string $password * @throws \InvalidArgumentException diff --git a/lib/public/Share/IManager.php b/lib/public/Share/IManager.php index 635ccc1483d..606e6429918 100644 --- a/lib/public/Share/IManager.php +++ b/lib/public/Share/IManager.php @@ -385,6 +385,22 @@ interface IManager { 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 * * @param string $userId diff --git a/lib/public/User/Backend/IGetRealUIDBackend.php b/lib/public/User/Backend/IGetRealUIDBackend.php index d724426e176..cc290eb6dc0 100644 --- a/lib/public/User/Backend/IGetRealUIDBackend.php +++ b/lib/public/User/Backend/IGetRealUIDBackend.php @@ -33,7 +33,7 @@ interface IGetRealUIDBackend { /** * Some backends accept different UIDs than what is the internal UID to be used. - * For example the database backend accepts differnt cased UIDs in all the functions + * For example the database backend accepts different cased UIDs in all the functions * but the internal UID that is to be used should be correctly cased. * * This little function makes sure that the used UID will be correct hen using the user object diff --git a/lib/public/User/Backend/ISearchKnownUsersBackend.php b/lib/public/User/Backend/ISearchKnownUsersBackend.php new file mode 100644 index 00000000000..89c7a49cd30 --- /dev/null +++ b/lib/public/User/Backend/ISearchKnownUsersBackend.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\User\Backend; + +/** + * @since 21.0.1 + */ +interface ISearchKnownUsersBackend { + + /** + * @param string $searcher + * @param string $pattern + * @param int|null $limit + * @param int|null $offset + * @return array + * @since 21.0.1 + */ + public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array; +} diff --git a/lib/public/UserInterface.php b/lib/public/UserInterface.php index 0479041e8de..42a18cca103 100644 --- a/lib/public/UserInterface.php +++ b/lib/public/UserInterface.php @@ -92,8 +92,8 @@ interface UserInterface { * Get a list of all display names and user ids. * * @param string $search - * @param string|null $limit - * @param string|null $offset + * @param int|null $limit + * @param int|null $offset * @return array an array of all displayNames (value) and the corresponding uids (key) * @since 4.5.0 */ |