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

LibreSign / libresign / 21008830184

14 Jan 2026 08:29PM UTC coverage: 44.286%. First build
21008830184

Pull #6436

github

web-flow
Merge 4bec3de23 into 9bd4c65c5
Pull Request #6436: feat: async parallel signing

390 of 923 new or added lines in 41 files covered. (42.25%)

7007 of 15822 relevant lines covered (44.29%)

4.93 hits per line

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

48.41
/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\IConfig;
24
use OCP\IL10N;
25
use OCP\IUserManager;
26

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

211
        public function getIdentifiedMethod(int $signRequestId): IIdentifyMethod {
212
                $matrix = $this->getIdentifyMethodsFromSignRequestId($signRequestId);
×
NEW
213
                $firstMethod = null;
×
214
                foreach ($matrix as $identifyMethods) {
×
215
                        foreach ($identifyMethods as $identifyMethod) {
×
NEW
216
                                if ($firstMethod === null) {
×
NEW
217
                                        $firstMethod = $identifyMethod;
×
218
                                }
219
                                if ($identifyMethod->getEntity()->getIdentifiedAtDate()) {
×
220
                                        return $identifyMethod;
×
221
                                }
222
                        }
223
                }
224
                // If no identified method found (e.g., clickToSign doesn't require identification),
225
                // return the first available method
NEW
226
                if ($firstMethod !== null) {
×
NEW
227
                        return $firstMethod;
×
228
                }
229
                throw new LibresignException($this->l10n->t('Invalid identification method'), 1);
×
230
        }
231

232
        public function getUserIdentifier(int $signRequestId): string {
NEW
233
                $identifyMethod = $this->getIdentifiedMethod($signRequestId);
×
NEW
234
                return $identifyMethod->getEntity()->getUniqueIdentifier();
×
235
        }
236

237
        public function deleteBySignRequestId(int $signRequestId): void {
238
                $this->identifyMethodMapper->deleteBySignRequestId($signRequestId);
3✔
239
                $this->clearCache();
3✔
240
        }
241

242
        public function getSignMethodsOfIdentifiedFactors(int $signRequestId): array {
243
                $matrix = $this->getIdentifyMethodsFromSignRequestId($signRequestId);
×
244
                $return = [];
×
245
                foreach ($matrix as $identifyMethods) {
×
246
                        foreach ($identifyMethods as $identifyMethod) {
×
247
                                $signatureMethods = $identifyMethod->getSignatureMethods();
×
248
                                foreach ($signatureMethods as $signatureMethod) {
×
249
                                        if (!$signatureMethod->isEnabled()) {
×
250
                                                continue;
×
251
                                        }
252
                                        $signatureMethod->setEntity($identifyMethod->getEntity());
×
253
                                        $return[$signatureMethod->getName()] = $signatureMethod->toArray();
×
254
                                }
255
                        }
256
                }
257
                return $return;
×
258
        }
259

260
        public function save(SignRequest $signRequest, bool $notify = true): void {
261
                foreach ($this->identifyMethods as $methods) {
×
262
                        foreach ($methods as $identifyMethod) {
×
263
                                $entity = $identifyMethod->getEntity();
×
264
                                $entity->setSignRequestId($signRequest->getId());
×
265
                                if ($entity->getId()) {
×
266
                                        $entity = $this->identifyMethodMapper->update($entity);
×
267
                                        if ($notify) {
×
268
                                                $identifyMethod->willNotifyUser(false);
×
269
                                                $identifyMethod->notify();
×
270
                                        }
271
                                } else {
272
                                        $entity = $this->identifyMethodMapper->insert($entity);
×
273
                                        if ($notify) {
×
274
                                                $identifyMethod->willNotifyUser(true);
×
275
                                                $identifyMethod->notify();
×
276
                                        }
277
                                }
278
                        }
279
                }
280
        }
281

282
        public function getIdentifyMethodsSettings(): array {
283
                if ($this->identifyMethodsSettings) {
13✔
284
                        return $this->identifyMethodsSettings;
1✔
285
                }
286
                $this->identifyMethodsSettings = [
13✔
287
                        $this->account->getSettings(),
13✔
288
                        $this->email->getSettings(),
13✔
289
                ];
13✔
290
                if ($this->signal->isTwofactorGatewayEnabled()) {
13✔
291
                        $this->identifyMethodsSettings[] = $this->signal->getSettings();
×
292
                }
293
                if ($this->sms->isTwofactorGatewayEnabled()) {
13✔
294
                        $this->identifyMethodsSettings[] = $this->sms->getSettings();
×
295
                }
296
                if ($this->telegram->isTwofactorGatewayEnabled()) {
13✔
297
                        $this->identifyMethodsSettings[] = $this->telegram->getSettings();
×
298
                }
299
                if ($this->Whatsapp->isTwofactorGatewayEnabled()) {
13✔
300
                        $this->identifyMethodsSettings[] = $this->Whatsapp->getSettings();
×
301
                }
302
                if ($this->xmpp->isTwofactorGatewayEnabled()) {
13✔
303
                        $this->identifyMethodsSettings[] = $this->xmpp->getSettings();
×
304
                }
305
                return $this->identifyMethodsSettings;
13✔
306
        }
307

308
        /**
309
         * Resolve UID from certificate chain data
310
         *
311
         * Extracts and resolves the identifier from certificate subject or extensions.
312
         * Supports fallbacks for older LibreSign versions and converts to standard
313
         * identifier format (account:uid or email:value).
314
         *
315
         * @param array $chainArr Certificate chain array with subject and extensions
316
         * @param string $host Host domain for email matching
317
         * @return string|null Resolved identifier in format "type:value" or null
318
         */
319
        public function resolveUid(array $chainArr, string $host): ?string {
320
                if (!empty($chainArr['subject']['UID'])) {
×
321
                        return $chainArr['subject']['UID'];
×
322
                }
323
                if (!empty($chainArr['subject']['CN'])) {
×
324
                        $cn = $chainArr['subject']['CN'];
×
325
                        if (is_array($cn)) {
×
326
                                $cn = $cn[0];
×
327
                        }
328
                        if (preg_match('/^(?<key>.*):(?<value>.*), /', (string)$cn, $matches)) {
×
329
                                return $matches['key'] . ':' . $matches['value'];
×
330
                        }
331
                }
332
                if (!empty($chainArr['extensions']['subjectAltName'])) {
×
333
                        $subjectAltName = $chainArr['extensions']['subjectAltName'];
×
334
                        if (is_array($subjectAltName)) {
×
335
                                $subjectAltName = $subjectAltName[0];
×
336
                        }
337
                        preg_match('/^(?<key>(email|account)):(?<value>.*)$/', (string)$subjectAltName, $matches);
×
338
                        if ($matches) {
×
339
                                if (str_ends_with($matches['value'], $host)) {
×
340
                                        $uid = str_replace('@' . $host, '', $matches['value']);
×
341
                                        $userFound = $this->userManager->get($uid);
×
342
                                        if ($userFound) {
×
343
                                                return 'account:' . $uid;
×
344
                                        } else {
345
                                                $userFound = $this->userManager->getByEmail($matches['value']);
×
346
                                                if ($userFound) {
×
347
                                                        $userFound = current($userFound);
×
348
                                                        return 'account:' . $userFound->getUID();
×
349
                                                } else {
350
                                                        return 'email:' . $matches['value'];
×
351
                                                }
352
                                        }
353
                                } else {
354
                                        $userFound = $this->userManager->getByEmail($matches['value']);
×
355
                                        if ($userFound) {
×
356
                                                $userFound = current($userFound);
×
357
                                                return 'account:' . $userFound->getUID();
×
358
                                        } else {
359
                                                $userFound = $this->userManager->get($matches['value']);
×
360
                                                if ($userFound) {
×
361
                                                        return 'account:' . $userFound->getUID();
×
362
                                                } else {
363
                                                        return $matches['key'] . ':' . $matches['value'];
×
364
                                                }
365
                                        }
366
                                }
367
                        }
368
                }
369
                return null;
×
370
        }
371
}
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