• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 21293358735

23 Jan 2026 04:30PM UTC coverage: 45.267%. First build
21293358735

Pull #6548

github

web-flow
Merge 34dc69686 into 3b6e6d7e8
Pull Request #6548: fix: reduce N+1

33 of 68 new or added lines in 6 files covered. (48.53%)

7446 of 16449 relevant lines covered (45.27%)

4.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.0
/lib/Service/IdentifyMethodService.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Service;
10

11
use OCA\Libresign\Db\IdentifyMethod;
12
use OCA\Libresign\Db\IdentifyMethodMapper;
13
use OCA\Libresign\Db\SignRequest;
14
use OCA\Libresign\Exception\LibresignException;
15
use OCA\Libresign\Service\IdentifyMethod\Account;
16
use OCA\Libresign\Service\IdentifyMethod\Email;
17
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
18
use OCA\Libresign\Service\IdentifyMethod\Signal;
19
use OCA\Libresign\Service\IdentifyMethod\Sms;
20
use OCA\Libresign\Service\IdentifyMethod\Telegram;
21
use OCA\Libresign\Service\IdentifyMethod\Whatsapp;
22
use OCA\Libresign\Service\IdentifyMethod\Xmpp;
23
use OCP\IL10N;
24
use OCP\IUserManager;
25

26
class IdentifyMethodService {
27
        public const IDENTIFY_ACCOUNT = 'account';
28
        public const IDENTIFY_EMAIL = 'email';
29
        public const IDENTIFY_SIGNAL = 'signal';
30
        public const IDENTIFY_TELEGRAM = 'telegram';
31
        public const IDENTIFY_SMS = 'sms';
32
        public const IDENTIFY_WHATSAPP = 'whatsapp';
33
        public const IDENTIFY_XMPP = 'xmpp';
34
        public const IDENTIFY_PASSWORD = 'password';
35
        public const IDENTIFY_CLICK_TO_SIGN = 'clickToSign';
36
        public const IDENTIFY_METHODS = [
37
                self::IDENTIFY_ACCOUNT,
38
                self::IDENTIFY_EMAIL,
39
                self::IDENTIFY_SIGNAL,
40
                self::IDENTIFY_TELEGRAM,
41
                self::IDENTIFY_SMS,
42
                self::IDENTIFY_WHATSAPP,
43
                self::IDENTIFY_XMPP,
44
                self::IDENTIFY_PASSWORD,
45
                self::IDENTIFY_CLICK_TO_SIGN,
46
        ];
47
        private bool $isRequest = true;
48
        private ?IdentifyMethod $currentIdentifyMethod = null;
49
        private array $identifyMethodsSettings = [];
50
        /**
51
         * @var array<string,array<IIdentifyMethod>>
52
         */
53
        private array $identifyMethods = [];
54

55
        public function __construct(
56
                private IdentifyMethodMapper $identifyMethodMapper,
57
                private IL10N $l10n,
58
                private IUserManager $userManager,
59
                private Account $account,
60
                private Email $email,
61
                private Signal $signal,
62
                private Sms $sms,
63
                private Telegram $telegram,
64
                private Whatsapp $Whatsapp,
65
                private Xmpp $xmpp,
66
        ) {
67
        }
61✔
68

69
        public function clearCache(): void {
70
                $this->identifyMethods = [];
13✔
71
                $this->currentIdentifyMethod = null;
13✔
72
        }
73

74
        public function setIsRequest(bool $isRequest): self {
75
                $this->isRequest = $isRequest;
2✔
76
                return $this;
2✔
77
        }
78

79
        public function getInstanceOfIdentifyMethod(string $name, ?string $identifyValue = null): IIdentifyMethod {
80
                if ($identifyValue && isset($this->identifyMethods[$name])) {
13✔
81
                        foreach ($this->identifyMethods[$name] as $identifyMethod) {
8✔
82
                                if ($identifyMethod->getEntity()->getIdentifierValue() === $identifyValue) {
8✔
83
                                        $identifyMethod = $this->mergeWithCurrentIdentifyMethod($identifyMethod);
8✔
84
                                        return $identifyMethod;
8✔
85
                                }
86
                        }
87
                }
88
                $identifyMethod = $this->getNewInstanceOfMethod($name);
13✔
89

90
                $entity = $identifyMethod->getEntity();
13✔
91
                if (!$entity->getId()) {
13✔
92
                        $entity->setIdentifierKey($name);
13✔
93
                        $entity->setIdentifierValue($identifyValue);
13✔
94
                        $entity->setMandatory($this->isMandatoryMethod($name) ? 1 : 0);
13✔
95
                }
96
                if ($identifyValue && $this->isRequest) {
13✔
97
                        $identifyMethod->validateToRequest();
13✔
98
                }
99

100
                $this->identifyMethods[$name][] = $identifyMethod;
13✔
101
                return $identifyMethod;
13✔
102
        }
103

104
        private function mergeWithCurrentIdentifyMethod(IIdentifyMethod $identifyMethod): IIdentifyMethod {
105
                if ($this->currentIdentifyMethod === null) {
8✔
106
                        return $identifyMethod;
×
107
                }
108
                if ($this->currentIdentifyMethod->getIdentifierKey() === $identifyMethod->getEntity()->getIdentifierKey()
8✔
109
                        && $this->currentIdentifyMethod->getIdentifierValue() === $identifyMethod->getEntity()->getIdentifierValue()
8✔
110
                ) {
111
                        $identifyMethod->setEntity($this->currentIdentifyMethod);
8✔
112
                }
113
                return $identifyMethod;
8✔
114
        }
115

116
        private function getNewInstanceOfMethod(string $name): IIdentifyMethod {
117
                $className = 'OCA\Libresign\Service\IdentifyMethod\\' . ucfirst($name);
13✔
118
                if (!class_exists($className)) {
13✔
119
                        $className = 'OCA\Libresign\Service\IdentifyMethod\\SignatureMethod\\' . ucfirst($name);
×
120
                        if (!class_exists($className)) {
×
121
                                // TRANSLATORS When is requested to a person to sign a file, is
122
                                // necessary identify what is the identification method. The
123
                                // identification method is used to define how will be the sign
124
                                // flow.
125
                                throw new LibresignException($this->l10n->t('Invalid identification method'));
×
126
                        }
127
                }
128
                /** @var IIdentifyMethod */
129
                $identifyMethod = clone \OCP\Server::get($className);
13✔
130
                if (empty($this->currentIdentifyMethod)) {
13✔
131
                        $identifyMethod->cleanEntity();
13✔
132
                } else {
133
                        $identifyMethod->setEntity($this->currentIdentifyMethod);
×
134
                }
135
                $identifyMethod->getSettings();
13✔
136
                return $identifyMethod;
13✔
137
        }
138

139
        private function setEntityData(string $method, string $identifyValue): void {
140
                // @todo Replace by enum when PHP 8.1 is the minimum version acceptable
141
                // at server. Check file lib/versioncheck.php of server repository
142
                if (!in_array($method, IdentifyMethodService::IDENTIFY_METHODS)) {
1✔
143
                        // TRANSLATORS When is requested to a person to sign a file, is
144
                        // necessary identify what is the identification method. The
145
                        // identification method is used to define how will be the sign
146
                        // flow.
147
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
×
148
                }
149
                $identifyMethod = $this->getInstanceOfIdentifyMethod($method, $identifyValue);
1✔
150
                $identifyMethod->validateToRequest();
1✔
151
        }
152

153
        public function setAllEntityData(array $user): void {
154
                foreach ($user['identify'] as $method => $identifyValue) {
1✔
155
                        $this->setEntityData($method, $identifyValue);
1✔
156
                }
157
        }
158

159
        private function isMandatoryMethod(string $methodName): bool {
160
                $settings = $this->getIdentifyMethodsSettings();
13✔
161
                foreach ($settings as $setting) {
13✔
162
                        if ($setting['name'] === $methodName) {
13✔
163
                                return $setting['mandatory'];
13✔
164
                        }
165
                }
166
                return false;
×
167
        }
168

169
        /**
170
         * @return array<IIdentifyMethod>
171
         */
172
        public function getByUserData(array $data) {
173
                $return = [];
13✔
174
                foreach ($data as $method => $identifyValue) {
13✔
175
                        $this->setCurrentIdentifyMethod();
13✔
176
                        $return[] = $this->getInstanceOfIdentifyMethod($method, $identifyValue);
13✔
177
                }
178
                return $return;
13✔
179
        }
180

181
        public function setCurrentIdentifyMethod(?IdentifyMethod $entity = null): self {
182
                $this->currentIdentifyMethod = $entity;
13✔
183
                return $this;
13✔
184
        }
185

186
        /**
187
         * @return array<string,array<IIdentifyMethod>>
188
         */
189
        public function getIdentifyMethodsFromSignRequestId(int $signRequestId): array {
190
                $entities = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequestId);
7✔
191
                foreach ($entities as $entity) {
7✔
192
                        $this->setCurrentIdentifyMethod($entity);
7✔
193
                        $this->getInstanceOfIdentifyMethod(
7✔
194
                                $entity->getIdentifierKey(),
7✔
195
                                $entity->getIdentifierValue(),
7✔
196
                        );
7✔
197
                }
198
                $return = [];
7✔
199
                foreach ($this->identifyMethods as $methodName => $list) {
7✔
200
                        foreach ($list as $method) {
7✔
201
                                if ($method->getEntity()->getSignRequestId() === $signRequestId) {
7✔
202
                                        $return[$methodName][] = $method;
7✔
203
                                }
204
                        }
205
                }
206
                return $return;
7✔
207
        }
208

209
        /**
210
         * @param int[] $signRequestIds
211
         * @return array<int, array<string,array<IIdentifyMethod>>>
212
         */
213
        public function getIdentifyMethodsFromSignRequestIds(array $signRequestIds): array {
214
                if (empty($signRequestIds)) {
3✔
NEW
215
                        return [];
×
216
                }
217

218
                $entitiesBySignRequest = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestIds($signRequestIds);
3✔
219

220
                foreach ($entitiesBySignRequest as $entities) {
3✔
221
                        foreach ($entities as $entity) {
3✔
NEW
222
                                $this->setCurrentIdentifyMethod($entity);
×
NEW
223
                                $this->getInstanceOfIdentifyMethod($entity->getIdentifierKey(), $entity->getIdentifierValue());
×
224
                        }
225
                }
226

227
                $results = [];
3✔
228
                foreach ($signRequestIds as $signRequestId) {
3✔
229
                        $results[$signRequestId] = [];
3✔
230
                        foreach ($this->identifyMethods as $methodName => $list) {
3✔
231
                                foreach ($list as $method) {
3✔
232
                                        if ($method->getEntity()->getSignRequestId() === $signRequestId) {
3✔
233
                                                $results[$signRequestId][$methodName][] = $method;
3✔
234
                                        }
235
                                }
236
                        }
237
                }
238

239
                return $results;
3✔
240
        }
241

242
        public function getIdentifiedMethod(int $signRequestId): IIdentifyMethod {
243
                $matrix = $this->getIdentifyMethodsFromSignRequestId($signRequestId);
1✔
244
                [$identifiedMethod, $firstMethod] = $this->findMethodsInMatrix($matrix);
1✔
245

246
                if ($identifiedMethod !== null) {
1✔
247
                        return $identifiedMethod;
1✔
248
                }
249

250
                if ($firstMethod !== null) {
×
251
                        return $firstMethod;
×
252
                }
253

254
                throw new LibresignException($this->l10n->t('Invalid identification method'), 1);
×
255
        }
256

257
        /**
258
         * @return array{?IIdentifyMethod, ?IIdentifyMethod} [identifiedMethod, firstMethod]
259
         */
260
        private function findMethodsInMatrix(array $matrix): array {
261
                $firstMethod = null;
14✔
262

263
                foreach ($matrix as $identifyMethods) {
14✔
264
                        foreach ($identifyMethods as $identifyMethod) {
11✔
265
                                $firstMethod ??= $identifyMethod;
11✔
266

267
                                if ($identifyMethod->getEntity()->getIdentifiedAtDate()) {
11✔
268
                                        return [$identifyMethod, $firstMethod];
7✔
269
                                }
270
                        }
271
                }
272

273
                return [null, $firstMethod];
7✔
274
        }
275

276
        public function getUserIdentifier(int $signRequestId): string {
277
                $identifyMethod = $this->getIdentifiedMethod($signRequestId);
×
278
                return $identifyMethod->getEntity()->getUniqueIdentifier();
×
279
        }
280

281
        public function deleteBySignRequestId(int $signRequestId): void {
282
                $this->identifyMethodMapper->deleteBySignRequestId($signRequestId);
3✔
283
                $this->clearCache();
3✔
284
        }
285

286
        public function getSignMethodsOfIdentifiedFactors(int $signRequestId): array {
287
                $matrix = $this->getIdentifyMethodsFromSignRequestId($signRequestId);
×
288
                $return = [];
×
289
                foreach ($matrix as $identifyMethods) {
×
290
                        foreach ($identifyMethods as $identifyMethod) {
×
291
                                $signatureMethods = $identifyMethod->getSignatureMethods();
×
292
                                foreach ($signatureMethods as $signatureMethod) {
×
293
                                        if (!$signatureMethod->isEnabled()) {
×
294
                                                continue;
×
295
                                        }
296
                                        $signatureMethod->setEntity($identifyMethod->getEntity());
×
297
                                        $return[$signatureMethod->getName()] = $signatureMethod->toArray();
×
298
                                }
299
                        }
300
                }
301
                return $return;
×
302
        }
303

304
        public function save(SignRequest $signRequest, bool $notify = true): void {
305
                foreach ($this->identifyMethods as $methods) {
×
306
                        foreach ($methods as $identifyMethod) {
×
307
                                $entity = $identifyMethod->getEntity();
×
308
                                $entity->setSignRequestId($signRequest->getId());
×
309
                                if ($entity->getId()) {
×
310
                                        $entity = $this->identifyMethodMapper->update($entity);
×
311
                                        if ($notify) {
×
312
                                                $identifyMethod->willNotifyUser(false);
×
313
                                                $identifyMethod->notify();
×
314
                                        }
315
                                } else {
316
                                        $entity = $this->identifyMethodMapper->insert($entity);
×
317
                                        if ($notify) {
×
318
                                                $identifyMethod->willNotifyUser(true);
×
319
                                                $identifyMethod->notify();
×
320
                                        }
321
                                }
322
                        }
323
                }
324
        }
325

326
        public function getIdentifyMethodsSettings(): array {
327
                if ($this->identifyMethodsSettings) {
13✔
328
                        return $this->identifyMethodsSettings;
2✔
329
                }
330
                $this->identifyMethodsSettings = [
13✔
331
                        $this->account->getSettings(),
13✔
332
                        $this->email->getSettings(),
13✔
333
                ];
13✔
334
                if ($this->signal->isTwofactorGatewayEnabled()) {
13✔
335
                        $this->identifyMethodsSettings[] = $this->signal->getSettings();
×
336
                }
337
                if ($this->sms->isTwofactorGatewayEnabled()) {
13✔
338
                        $this->identifyMethodsSettings[] = $this->sms->getSettings();
×
339
                }
340
                if ($this->telegram->isTwofactorGatewayEnabled()) {
13✔
341
                        $this->identifyMethodsSettings[] = $this->telegram->getSettings();
×
342
                }
343
                if ($this->Whatsapp->isTwofactorGatewayEnabled()) {
13✔
344
                        $this->identifyMethodsSettings[] = $this->Whatsapp->getSettings();
×
345
                }
346
                if ($this->xmpp->isTwofactorGatewayEnabled()) {
13✔
347
                        $this->identifyMethodsSettings[] = $this->xmpp->getSettings();
×
348
                }
349
                return $this->identifyMethodsSettings;
13✔
350
        }
351

352
        /**
353
         * Resolve UID from certificate chain data
354
         *
355
         * Extracts and resolves the identifier from certificate subject or extensions.
356
         * Supports fallbacks for older LibreSign versions and converts to standard
357
         * identifier format (account:uid or email:value).
358
         *
359
         * @param array $chainArr Certificate chain array with subject and extensions
360
         * @param string $host Host domain for email matching
361
         * @return string|null Resolved identifier in format "type:value" or null
362
         */
363
        public function resolveUid(array $chainArr, string $host): ?string {
364
                if (!empty($chainArr['subject']['UID'])) {
×
365
                        return $chainArr['subject']['UID'];
×
366
                }
367
                if (!empty($chainArr['subject']['CN'])) {
×
368
                        $cn = $chainArr['subject']['CN'];
×
369
                        if (is_array($cn)) {
×
370
                                $cn = $cn[0];
×
371
                        }
372
                        if (preg_match('/^(?<key>.*):(?<value>.*), /', (string)$cn, $matches)) {
×
373
                                return $matches['key'] . ':' . $matches['value'];
×
374
                        }
375
                }
376
                if (!empty($chainArr['extensions']['subjectAltName'])) {
×
377
                        $subjectAltName = $chainArr['extensions']['subjectAltName'];
×
378
                        if (is_array($subjectAltName)) {
×
379
                                $subjectAltName = $subjectAltName[0];
×
380
                        }
381
                        preg_match('/^(?<key>(email|account)):(?<value>.*)$/', (string)$subjectAltName, $matches);
×
382
                        if ($matches) {
×
383
                                if (str_ends_with($matches['value'], $host)) {
×
384
                                        $uid = str_replace('@' . $host, '', $matches['value']);
×
385
                                        $userFound = $this->userManager->get($uid);
×
386
                                        if ($userFound) {
×
387
                                                return 'account:' . $uid;
×
388
                                        } else {
389
                                                $userFound = $this->userManager->getByEmail($matches['value']);
×
390
                                                if ($userFound) {
×
391
                                                        $userFound = current($userFound);
×
392
                                                        return 'account:' . $userFound->getUID();
×
393
                                                } else {
394
                                                        return 'email:' . $matches['value'];
×
395
                                                }
396
                                        }
397
                                } else {
398
                                        $userFound = $this->userManager->getByEmail($matches['value']);
×
399
                                        if ($userFound) {
×
400
                                                $userFound = current($userFound);
×
401
                                                return 'account:' . $userFound->getUID();
×
402
                                        } else {
403
                                                $userFound = $this->userManager->get($matches['value']);
×
404
                                                if ($userFound) {
×
405
                                                        return 'account:' . $userFound->getUID();
×
406
                                                } else {
407
                                                        return $matches['key'] . ':' . $matches['value'];
×
408
                                                }
409
                                        }
410
                                }
411
                        }
412
                }
413
                return null;
×
414
        }
415
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc