Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.52% covered (success)
94.52%
69 / 73
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
AccountPhonePlugin
94.52% covered (success)
94.52%
69 / 73
50.00% covered (danger)
50.00%
1 / 2
27.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 search
94.44% covered (success)
94.44%
68 / 72
0.00% covered (danger)
0.00%
0 / 1
26.12
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Collaboration\Collaborators;
10
11use OC\KnownUser\KnownUserService;
12use OCA\Libresign\Service\Identify\SearchNormalizer;
13use OCA\Libresign\Service\Identify\SignerSearchContext;
14use OCA\Libresign\Service\IdentifyMethodService;
15use OCP\Accounts\IAccountManager;
16use OCP\Collaboration\Collaborators\ISearchPlugin;
17use OCP\Collaboration\Collaborators\ISearchResult;
18use OCP\Collaboration\Collaborators\SearchResultType;
19use OCP\IAppConfig;
20use OCP\IGroupManager;
21use OCP\IUserManager;
22use OCP\IUserSession;
23
24class AccountPhonePlugin implements ISearchPlugin {
25    public const TYPE_SIGNER_ACCOUNT_PHONE = 51;
26
27    public function __construct(
28        private IAppConfig $appConfig,
29        private IAccountManager $accountManager,
30        private IGroupManager $groupManager,
31        private IUserSession $userSession,
32        private KnownUserService $knownUserService,
33        private IUserManager $userManager,
34        private SignerSearchContext $searchContext,
35        private SearchNormalizer $searchNormalizer,
36    ) {
37    }
38
39    /**
40     * {@inheritdoc}
41     */
42    #[\Override]
43    public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
44        $method = $this->searchContext->getMethod();
45        $search = trim((string)$search);
46
47        if ($search === '' || !in_array($method, IdentifyMethodService::IDENTIFY_PHONE_METHODS, true)) {
48            return false;
49        }
50
51        $currentUser = $this->userSession->getUser();
52        if ($currentUser === null) {
53            return false;
54        }
55
56        $shareeEnumeration = $this->appConfig->getValueString('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
57        $shareeEnumerationFullMatch = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
58        if (!$shareeEnumeration && !$shareeEnumerationFullMatch) {
59            return false;
60        }
61
62        $shareeEnumerationRestrictToGroup = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
63        $shareeEnumerationRestrictToPhone = $this->appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
64        $shareWithGroupOnly = $this->appConfig->getValueString('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
65        $shareWithGroupOnlyExcludeGroupsList = json_decode(
66            $this->appConfig->getValueString('core', 'shareapi_only_share_with_group_members_exclude_group_list', '[]'),
67            true,
68            512,
69            JSON_THROW_ON_ERROR
70        ) ?? [];
71        $allowedGroups = array_diff($this->groupManager->getUserGroupIds($currentUser), $shareWithGroupOnlyExcludeGroupsList);
72
73        $matches = $this->accountManager->searchUsers(IAccountManager::PROPERTY_PHONE, [$search]);
74        $items = [];
75        foreach ($matches as $phone => $userId) {
76            // Filter out users with phone numbers that cannot be normalized
77            $normalizedPhone = $this->searchNormalizer->tryNormalizePhoneNumber((string)$phone, $method);
78            if ($normalizedPhone === null) {
79                continue;
80            }
81
82            $userId = (string)$userId;
83            if ($userId === $currentUser->getUID()) {
84                continue;
85            }
86            $user = $this->userManager->get($userId);
87            if ($user === null || !$user->isEnabled()) {
88                continue;
89            }
90            $userGroups = $this->groupManager->getUserGroupIds($user);
91            $inAllowedGroup = array_intersect($allowedGroups, $userGroups) !== [];
92
93            if ($shareeEnumeration) {
94                $allowedByRestriction = true;
95                if ($shareeEnumerationRestrictToGroup || $shareeEnumerationRestrictToPhone) {
96                    $allowedByRestriction = false;
97                    if ($shareeEnumerationRestrictToGroup && $inAllowedGroup) {
98                        $allowedByRestriction = true;
99                    }
100                    if ($shareeEnumerationRestrictToPhone
101                        && $this->knownUserService->isKnownToUser($currentUser->getUID(), $userId)) {
102                        $allowedByRestriction = true;
103                    }
104                }
105                if (!$allowedByRestriction) {
106                    continue;
107                }
108            } elseif (!$shareeEnumerationFullMatch) {
109                continue;
110            }
111
112            if ($shareWithGroupOnly && !$inAllowedGroup) {
113                continue;
114            }
115
116            $displayName = $user->getDisplayName() !== '' ? $user->getDisplayName() : $userId;
117            $items[] = [
118                'label' => $displayName,
119                'shareWithDisplayNameUnique' => $normalizedPhone,
120                'method' => $method,
121                'value' => [
122                    'shareType' => self::TYPE_SIGNER_ACCOUNT_PHONE,
123                    'shareWith' => $normalizedPhone,
124                ],
125            ];
126        }
127
128        $hasMore = count($items) > ($offset + $limit);
129        $pagedItems = array_slice($items, $offset, $limit);
130
131        $result = ['wide' => [], 'exact' => []];
132        $searchLower = strtolower($search);
133        foreach ($pagedItems as $item) {
134            if (strtolower($item['shareWithDisplayNameUnique']) === $searchLower
135                || strtolower($item['label']) === $searchLower
136            ) {
137                $result['exact'][] = $item;
138            } else {
139                $result['wide'][] = $item;
140            }
141        }
142
143        $type = new SearchResultType('account-phone');
144        $searchResult->addResultSet($type, $result['wide'], $result['exact']);
145
146        return $hasMore;
147    }
148}