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

LibreSign / libresign / 21004797917

14 Jan 2026 06:08PM UTC coverage: 43.74%. First build
21004797917

Pull #6436

github

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

294 of 860 new or added lines in 35 files covered. (34.19%)

6921 of 15823 relevant lines covered (43.74%)

4.86 hits per line

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

49.03
/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 deleteBySignRequestId(int $signRequestId): void {
233
                $this->identifyMethodMapper->deleteBySignRequestId($signRequestId);
3✔
234
                $this->clearCache();
3✔
235
        }
236

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

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

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

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