Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.91% covered (warning)
84.91%
197 / 232
65.52% covered (warning)
65.52%
19 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
IdentifyMethodService
84.91% covered (warning)
84.91%
197 / 232
65.52% covered (warning)
65.52%
19 / 29
134.34
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
 clearCache
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setIsRequest
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getInstanceOfIdentifyMethod
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
8
 mergeWithCurrentIdentifyMethod
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getNewInstanceOfMethod
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 exists
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 resolveTwofactorGatewayName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setEntityData
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setAllEntityData
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
12.89
 resolveMethodRequirement
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getByUserData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setCurrentIdentifyMethod
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getIdentifyMethodsFromSignRequestId
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getIdentifyMethodsFromSignRequestIds
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
8.51
 getIdentifiedMethod
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 findMethodsInMatrix
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getUserIdentifier
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 deleteBySignRequestId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSignMethodsOfIdentifiedFactors
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSignMethodsOfAccount
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getSignMethodsOfIdentifyMethods
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 save
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 getIdentifyMethodsCatalogSettings
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
8.86
 getIdentifyMethodsSettings
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
8.19
 getFriendlyNamesMap
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultIdentifyMethodsPolicy
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 buildDefaultPolicyFactorFromSettings
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
7
 resolveUid
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service;
10
11use OCA\Libresign\Db\IdentifyMethod;
12use OCA\Libresign\Db\IdentifyMethodMapper;
13use OCA\Libresign\Db\SignRequest;
14use OCA\Libresign\Enum\IdentifyMethodRequirement;
15use OCA\Libresign\Exception\LibresignException;
16use OCA\Libresign\ResponseDefinitions;
17use OCA\Libresign\Service\IdentifyMethod\Account;
18use OCA\Libresign\Service\IdentifyMethod\Email;
19use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
20use OCA\Libresign\Service\IdentifyMethod\Signal;
21use OCA\Libresign\Service\IdentifyMethod\Sms;
22use OCA\Libresign\Service\IdentifyMethod\Telegram;
23use OCA\Libresign\Service\IdentifyMethod\Whatsapp;
24use OCA\Libresign\Service\IdentifyMethod\Whatsappbusiness;
25use OCA\Libresign\Service\IdentifyMethod\Xmpp;
26use OCP\IL10N;
27use OCP\IUserManager;
28
29/**
30 * @psalm-import-type LibresignIdentifyMethodAdminSetting from ResponseDefinitions
31 * @psalm-import-type LibresignPolicySnapshotIdentifyMethodFactor from ResponseDefinitions
32 * @psalm-import-type LibresignPolicySnapshotSignatureMethodSetting from ResponseDefinitions
33 */
34class IdentifyMethodService {
35    public const IDENTIFY_ACCOUNT = 'account';
36    public const IDENTIFY_EMAIL = 'email';
37    public const IDENTIFY_SIGNAL = 'signal';
38    public const IDENTIFY_TELEGRAM = 'telegram';
39    public const IDENTIFY_SMS = 'sms';
40    public const IDENTIFY_WHATSAPP = 'whatsapp';
41    public const IDENTIFY_WHATSAPP_BUSINESS = 'whatsappbusiness';
42    public const IDENTIFY_XMPP = 'xmpp';
43    public const IDENTIFY_PHONE_METHODS = [
44        self::IDENTIFY_WHATSAPP,
45        self::IDENTIFY_WHATSAPP_BUSINESS,
46        self::IDENTIFY_SMS,
47        self::IDENTIFY_TELEGRAM,
48        self::IDENTIFY_SIGNAL,
49    ];
50    public const IDENTIFY_TWOFACTOR_GATEWAY_METHODS = [
51        self::IDENTIFY_SMS,
52        self::IDENTIFY_SIGNAL,
53        self::IDENTIFY_TELEGRAM,
54        self::IDENTIFY_WHATSAPP,
55        self::IDENTIFY_WHATSAPP_BUSINESS,
56        self::IDENTIFY_XMPP,
57    ];
58    public const IDENTIFY_PASSWORD = 'password';
59    public const IDENTIFY_CLICK_TO_SIGN = 'clickToSign';
60    public const IDENTIFY_METHODS = [
61        self::IDENTIFY_ACCOUNT,
62        self::IDENTIFY_EMAIL,
63        self::IDENTIFY_SIGNAL,
64        self::IDENTIFY_TELEGRAM,
65        self::IDENTIFY_SMS,
66        self::IDENTIFY_WHATSAPP,
67        self::IDENTIFY_WHATSAPP_BUSINESS,
68        self::IDENTIFY_XMPP,
69        self::IDENTIFY_PASSWORD,
70        self::IDENTIFY_CLICK_TO_SIGN,
71    ];
72    private bool $isRequest = true;
73    private ?IdentifyMethod $currentIdentifyMethod = null;
74    /** @var list<LibresignIdentifyMethodAdminSetting> */
75    private array $identifyMethodsSettings = [];
76    /** @var list<LibresignIdentifyMethodAdminSetting> */
77    private array $identifyMethodsCatalogSettings = [];
78    /**
79     * @var array<string,array<IIdentifyMethod>>
80     */
81    private array $identifyMethods = [];
82
83    public function __construct(
84        private IdentifyMethodMapper $identifyMethodMapper,
85        private IL10N $l10n,
86        private IUserManager $userManager,
87        private Account $account,
88        private Email $email,
89        private Signal $signal,
90        private Sms $sms,
91        private Telegram $telegram,
92        private Whatsapp $Whatsapp,
93        private Whatsappbusiness $whatsappbusiness,
94        private Xmpp $xmpp,
95        private SubjectAlternativeNameService $subjectAlternativeNameService,
96    ) {
97    }
98
99    public function clearCache(): void {
100        $this->identifyMethods = [];
101        $this->currentIdentifyMethod = null;
102        $this->identifyMethodsSettings = [];
103        $this->identifyMethodsCatalogSettings = [];
104    }
105
106    public function setIsRequest(bool $isRequest): self {
107        $this->isRequest = $isRequest;
108        return $this;
109    }
110
111    public function getInstanceOfIdentifyMethod(string $name, ?string $identifyValue = null, ?string $requirement = null): IIdentifyMethod {
112        if ($identifyValue && isset($this->identifyMethods[$name])) {
113            foreach ($this->identifyMethods[$name] as $identifyMethod) {
114                if ($identifyMethod->getEntity()->getIdentifierValue() === $identifyValue) {
115                    $identifyMethod = $this->mergeWithCurrentIdentifyMethod($identifyMethod);
116                    return $identifyMethod;
117                }
118            }
119        }
120        $identifyMethod = $this->getNewInstanceOfMethod($name);
121
122        $entity = $identifyMethod->getEntity();
123        if (!$entity->getId()) {
124            $entity->setIdentifierKey($name);
125            $entity->setIdentifierValue($identifyValue);
126            $entity->setRequirement($requirement ?? $this->resolveMethodRequirement($name));
127        }
128        if ($identifyValue && $this->isRequest) {
129            $identifyMethod->validateToRequest();
130        }
131
132        $this->identifyMethods[$name][] = $identifyMethod;
133        return $identifyMethod;
134    }
135
136    private function mergeWithCurrentIdentifyMethod(IIdentifyMethod $identifyMethod): IIdentifyMethod {
137        if ($this->currentIdentifyMethod === null) {
138            return $identifyMethod;
139        }
140        if ($this->currentIdentifyMethod->getIdentifierKey() === $identifyMethod->getEntity()->getIdentifierKey()
141            && $this->currentIdentifyMethod->getIdentifierValue() === $identifyMethod->getEntity()->getIdentifierValue()
142        ) {
143            $identifyMethod->setEntity($this->currentIdentifyMethod);
144        }
145        return $identifyMethod;
146    }
147
148    private function getNewInstanceOfMethod(string $name): IIdentifyMethod {
149        $className = 'OCA\Libresign\Service\IdentifyMethod\\' . ucfirst($name);
150        if (!class_exists($className)) {
151            $className = 'OCA\Libresign\Service\IdentifyMethod\\SignatureMethod\\' . ucfirst($name);
152            if (!class_exists($className)) {
153                // TRANSLATORS When is requested to a person to sign a file, is
154                // necessary identify what is the identification method. The
155                // identification method is used to define how will be the sign
156                // flow.
157                throw new LibresignException($this->l10n->t('Invalid identification method'));
158            }
159        }
160        /** @var IIdentifyMethod */
161        $identifyMethod = clone \OCP\Server::get($className);
162        if (empty($this->currentIdentifyMethod)) {
163            $identifyMethod->cleanEntity();
164        } else {
165            $identifyMethod->setEntity($this->currentIdentifyMethod);
166        }
167        $identifyMethod->getSettings();
168        return $identifyMethod;
169    }
170
171    public function exists(string $name): bool {
172        $className = 'OCA\\Libresign\\Service\\IdentifyMethod\\' . ucfirst($name);
173        if (class_exists($className)) {
174            return true;
175        }
176        return class_exists('OCA\\Libresign\\Service\\IdentifyMethod\\SignatureMethod\\' . ucfirst($name));
177    }
178
179    public static function resolveTwofactorGatewayName(string $identifyMethod): string {
180        return match ($identifyMethod) {
181            self::IDENTIFY_WHATSAPP => 'gowhatsapp',
182            default => strtolower($identifyMethod),
183        };
184    }
185
186    private function setEntityData(string $method, string $identifyValue, ?string $requirement = null): void {
187        $identifyMethod = $this->getInstanceOfIdentifyMethod($method, $identifyValue, $requirement);
188        $identifyMethod->validateToRequest();
189    }
190
191    public function setAllEntityData(array $user): void {
192        foreach (($user['identifyMethods'] ?? []) as $identifyMethod) {
193            if (!is_array($identifyMethod) || !isset($identifyMethod['method'], $identifyMethod['value'])) {
194                continue;
195            }
196            $requirement = isset($identifyMethod['requirement']) && is_string($identifyMethod['requirement'])
197                ? $identifyMethod['requirement']
198                : null;
199            if ($requirement === null && array_key_exists('mandatory', $identifyMethod)) {
200                $requirement = ((int)$identifyMethod['mandatory']) === 1
201                    ? IdentifyMethodRequirement::REQUIRED->value
202                    : IdentifyMethodRequirement::OPTIONAL->value;
203            }
204            $this->setEntityData($identifyMethod['method'], $identifyMethod['value'], $requirement);
205        }
206    }
207
208    private function resolveMethodRequirement(string $methodName): string {
209        $settings = $this->getIdentifyMethodsSettings();
210        foreach ($settings as $setting) {
211            if ($setting['name'] === $methodName) {
212                $requirement = IdentifyMethodRequirement::tryFrom((string)($setting['requirement'] ?? ''));
213                return $requirement?->value ?? IdentifyMethodRequirement::OPTIONAL->value;
214            }
215        }
216        return IdentifyMethodRequirement::OPTIONAL->value;
217    }
218
219    /**
220     * @return array<IIdentifyMethod>
221     */
222    public function getByUserData(array $data) {
223        $return = [];
224        foreach ($data as $method => $identifyValue) {
225            $this->setCurrentIdentifyMethod();
226            $return[] = $this->getInstanceOfIdentifyMethod($method, $identifyValue);
227        }
228        return $return;
229    }
230
231    public function setCurrentIdentifyMethod(?IdentifyMethod $entity = null): self {
232        $this->currentIdentifyMethod = $entity;
233        return $this;
234    }
235
236    /**
237     * @return array<string,array<IIdentifyMethod>>
238     */
239    public function getIdentifyMethodsFromSignRequestId(int $signRequestId): array {
240        $entities = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequestId);
241        foreach ($entities as $entity) {
242            $this->setCurrentIdentifyMethod($entity);
243            $this->getInstanceOfIdentifyMethod(
244                $entity->getIdentifierKey(),
245                $entity->getIdentifierValue(),
246            );
247        }
248        $return = [];
249        foreach ($this->identifyMethods as $methodName => $list) {
250            foreach ($list as $method) {
251                if ($method->getEntity()->getSignRequestId() === $signRequestId) {
252                    $return[$methodName][] = $method;
253                }
254            }
255        }
256        return $return;
257    }
258
259    /**
260     * @param int[] $signRequestIds
261     * @return array<int, array<string,array<IIdentifyMethod>>>
262     */
263    public function getIdentifyMethodsFromSignRequestIds(array $signRequestIds): array {
264        if (empty($signRequestIds)) {
265            return [];
266        }
267
268        $entitiesBySignRequest = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestIds($signRequestIds);
269
270        foreach ($entitiesBySignRequest as $entities) {
271            foreach ($entities as $entity) {
272                $this->setCurrentIdentifyMethod($entity);
273                $this->getInstanceOfIdentifyMethod($entity->getIdentifierKey(), $entity->getIdentifierValue());
274            }
275        }
276
277        $results = [];
278        foreach ($signRequestIds as $signRequestId) {
279            $results[$signRequestId] = [];
280            foreach ($this->identifyMethods as $methodName => $list) {
281                foreach ($list as $method) {
282                    if ($method->getEntity()->getSignRequestId() === $signRequestId) {
283                        $results[$signRequestId][$methodName][] = $method;
284                    }
285                }
286            }
287        }
288
289        return $results;
290    }
291
292    public function getIdentifiedMethod(int $signRequestId): IIdentifyMethod {
293        $matrix = $this->getIdentifyMethodsFromSignRequestId($signRequestId);
294        [$identifiedMethod, $firstMethod] = $this->findMethodsInMatrix($matrix);
295
296        if ($identifiedMethod !== null) {
297            return $identifiedMethod;
298        }
299
300        if ($firstMethod !== null) {
301            return $firstMethod;
302        }
303
304        // TRANSLATORS Error shown when the chosen method to identify the signer (account, email, SMS, etc.) is invalid.
305        throw new LibresignException($this->l10n->t('Invalid identification method'), 1);
306    }
307
308    /**
309     * @return array{?IIdentifyMethod, ?IIdentifyMethod} [identifiedMethod, firstMethod]
310     */
311    private function findMethodsInMatrix(array $matrix): array {
312        $firstMethod = null;
313
314        foreach ($matrix as $identifyMethods) {
315            foreach ($identifyMethods as $identifyMethod) {
316                $firstMethod ??= $identifyMethod;
317
318                if ($identifyMethod->getEntity()->getIdentifiedAtDate()) {
319                    return [$identifyMethod, $firstMethod];
320                }
321            }
322        }
323
324        return [null, $firstMethod];
325    }
326
327    public function getUserIdentifier(int $signRequestId): string {
328        $identifyMethod = $this->getIdentifiedMethod($signRequestId);
329        return $identifyMethod->getEntity()->getUniqueIdentifier();
330    }
331
332    public function deleteBySignRequestId(int $signRequestId): void {
333        $this->identifyMethodMapper->deleteBySignRequestId($signRequestId);
334        $this->clearCache();
335    }
336
337    public function getSignMethodsOfIdentifiedFactors(int $signRequestId): array {
338        $matrix = $this->getIdentifyMethodsFromSignRequestId($signRequestId);
339        return $this->getSignMethodsOfIdentifyMethods($matrix);
340    }
341
342    /**
343     * Built in memory: nothing is persisted and the instance is not
344     * registered in the list that save() iterates.
345     */
346    public function getSignMethodsOfAccount(string $uid): array {
347        $identifyMethod = $this->getNewInstanceOfMethod(self::IDENTIFY_ACCOUNT);
348        $identifyMethod->cleanEntity();
349        $identifyMethod->getEntity()->setIdentifierValue($uid);
350        return $this->getSignMethodsOfIdentifyMethods([self::IDENTIFY_ACCOUNT => [$identifyMethod]]);
351    }
352
353    /**
354     * @param array<string,array<IIdentifyMethod>> $matrix
355     */
356    private function getSignMethodsOfIdentifyMethods(array $matrix): array {
357        $return = [];
358        foreach ($matrix as $identifyMethods) {
359            foreach ($identifyMethods as $identifyMethod) {
360                $signatureMethods = $identifyMethod->getSignatureMethods();
361                foreach ($signatureMethods as $signatureMethod) {
362                    if (!$signatureMethod->isEnabled()) {
363                        continue;
364                    }
365                    $signatureMethod->setEntity($identifyMethod->getEntity());
366                    $signatureMethodData = $signatureMethod->toArray();
367                    $return[$signatureMethod->getName()] = $signatureMethodData;
368                }
369            }
370        }
371        return $return;
372    }
373
374    public function save(SignRequest $signRequest, bool $notify = true): void {
375        foreach ($this->identifyMethods as $methods) {
376            foreach ($methods as $identifyMethod) {
377                $entity = $identifyMethod->getEntity();
378                $entity->setSignRequestId($signRequest->getId());
379                if ($entity->getId()) {
380                    $entity = $this->identifyMethodMapper->update($entity);
381                    if ($notify) {
382                        $identifyMethod->willNotifyUser(false);
383                        $identifyMethod->notify();
384                    }
385                } else {
386                    $entity = $this->identifyMethodMapper->insert($entity);
387                    if ($notify) {
388                        $identifyMethod->willNotifyUser(true);
389                        $identifyMethod->notify();
390                    }
391                }
392            }
393        }
394    }
395
396    /**
397     * @return array
398     * @psalm-return list<LibresignIdentifyMethodAdminSetting>
399     */
400    public function getIdentifyMethodsCatalogSettings(): array {
401        if ($this->identifyMethodsCatalogSettings) {
402            /** @var list<LibresignIdentifyMethodAdminSetting> $cachedCatalogSettings */
403            $cachedCatalogSettings = $this->identifyMethodsCatalogSettings;
404            return $cachedCatalogSettings;
405        }
406
407        $this->identifyMethodsCatalogSettings = [
408            $this->account->getDefaultSettings(),
409            $this->email->getDefaultSettings(),
410        ];
411        if ($this->signal->isTwofactorGatewayEnabled()) {
412            $this->identifyMethodsCatalogSettings[] = $this->signal->getDefaultSettings();
413        }
414        if ($this->sms->isTwofactorGatewayEnabled()) {
415            $this->identifyMethodsCatalogSettings[] = $this->sms->getDefaultSettings();
416        }
417        if ($this->telegram->isTwofactorGatewayEnabled()) {
418            $this->identifyMethodsCatalogSettings[] = $this->telegram->getDefaultSettings();
419        }
420        if ($this->Whatsapp->isTwofactorGatewayEnabled()) {
421            $this->identifyMethodsCatalogSettings[] = $this->Whatsapp->getDefaultSettings();
422        }
423        if ($this->whatsappbusiness->isTwofactorGatewayEnabled()) {
424            $this->identifyMethodsCatalogSettings[] = $this->whatsappbusiness->getDefaultSettings();
425        }
426        if ($this->xmpp->isTwofactorGatewayEnabled()) {
427            $this->identifyMethodsCatalogSettings[] = $this->xmpp->getDefaultSettings();
428        }
429
430        /** @var list<LibresignIdentifyMethodAdminSetting> $catalogSettings */
431        $catalogSettings = $this->identifyMethodsCatalogSettings;
432        return $catalogSettings;
433    }
434
435    /**
436     * @return array
437     * @psalm-return list<LibresignIdentifyMethodAdminSetting>
438     */
439    public function getIdentifyMethodsSettings(): array {
440        if ($this->identifyMethodsSettings) {
441            /** @var list<LibresignIdentifyMethodAdminSetting> $cachedIdentifyMethodsSettings */
442            $cachedIdentifyMethodsSettings = $this->identifyMethodsSettings;
443            return $cachedIdentifyMethodsSettings;
444        }
445        $this->identifyMethodsSettings = [
446            $this->account->getSettings(),
447            $this->email->getSettings(),
448        ];
449        if ($this->signal->isTwofactorGatewayEnabled()) {
450            $this->identifyMethodsSettings[] = $this->signal->getSettings();
451        }
452        if ($this->sms->isTwofactorGatewayEnabled()) {
453            $this->identifyMethodsSettings[] = $this->sms->getSettings();
454        }
455        if ($this->telegram->isTwofactorGatewayEnabled()) {
456            $this->identifyMethodsSettings[] = $this->telegram->getSettings();
457        }
458        if ($this->Whatsapp->isTwofactorGatewayEnabled()) {
459            $this->identifyMethodsSettings[] = $this->Whatsapp->getSettings();
460        }
461        if ($this->whatsappbusiness->isTwofactorGatewayEnabled()) {
462            $this->identifyMethodsSettings[] = $this->whatsappbusiness->getSettings();
463        }
464        if ($this->xmpp->isTwofactorGatewayEnabled()) {
465            $this->identifyMethodsSettings[] = $this->xmpp->getSettings();
466        }
467        /** @var list<LibresignIdentifyMethodAdminSetting> $identifyMethodsSettings */
468        $identifyMethodsSettings = $this->identifyMethodsSettings;
469        return $identifyMethodsSettings;
470    }
471
472    /** @return array<string, string> */
473    public function getFriendlyNamesMap(): array {
474        return [
475            $this->account->getName() => $this->account->getFriendlyName(),
476            $this->email->getName() => $this->email->getFriendlyName(),
477            $this->signal->getName() => $this->signal->getFriendlyName(),
478            $this->sms->getName() => $this->sms->getFriendlyName(),
479            $this->telegram->getName() => $this->telegram->getFriendlyName(),
480            $this->Whatsapp->getName() => $this->Whatsapp->getFriendlyName(),
481            $this->whatsappbusiness->getName() => $this->whatsappbusiness->getFriendlyName(),
482            $this->xmpp->getName() => $this->xmpp->getFriendlyName(),
483        ];
484    }
485
486    /**
487     * Get default identify methods policy seed
488     *
489     * Returns a legitimate default configuration with account and email methods
490     * when no policy is explicitly configured. This provides a reasonable baseline
491     * for new rules without hardcoding payload values in this service.
492     *
493     * @return array Default identify methods factors array
494     * @psalm-return list<LibresignPolicySnapshotIdentifyMethodFactor>
495     */
496    public function getDefaultIdentifyMethodsPolicy(): array {
497        return [
498            $this->buildDefaultPolicyFactorFromSettings($this->account->getDefaultSettings()),
499            $this->buildDefaultPolicyFactorFromSettings($this->email->getDefaultSettings()),
500        ];
501    }
502
503    /**
504     * @param LibresignIdentifyMethodAdminSetting $settings
505     * @return array
506     * @psalm-return LibresignPolicySnapshotIdentifyMethodFactor
507     */
508    private function buildDefaultPolicyFactorFromSettings(array $settings): array {
509        /** @var array<string, LibresignPolicySnapshotSignatureMethodSetting> $signatureMethods */
510        $signatureMethods = [];
511        $signatureMethodEnabled = '';
512
513        foreach ($settings['signatureMethods'] ?? [] as $signatureMethodName => $signatureMethodConfig) {
514            $isEnabled = (bool)($signatureMethodConfig['enabled'] ?? false);
515            $normalizedSignatureMethod = is_array($signatureMethodConfig)
516                ? $signatureMethodConfig
517                : [];
518            $normalizedSignatureMethod['enabled'] = $isEnabled;
519            $normalizedSignatureMethod['name'] = $signatureMethodName;
520            $normalizedSignatureMethod['label'] = $normalizedSignatureMethod['label'] ?? $signatureMethodName;
521
522            /** @var LibresignPolicySnapshotSignatureMethodSetting $normalizedSignatureMethod */
523            $signatureMethods[$signatureMethodName] = $normalizedSignatureMethod;
524
525            if ($signatureMethodEnabled === '' && $isEnabled) {
526                $signatureMethodEnabled = $signatureMethodName;
527            }
528        }
529
530        if ($signatureMethodEnabled === '' && !empty($signatureMethods)) {
531            $signatureMethodEnabled = (string)array_key_first($signatureMethods);
532        }
533
534        $requirement = IdentifyMethodRequirement::tryFrom((string)($settings['requirement'] ?? ''));
535
536        /** @var LibresignPolicySnapshotIdentifyMethodFactor $defaultPolicyFactor */
537        $defaultPolicyFactor = [
538            'name' => $settings['name'],
539            'enabled' => (bool)($settings['enabled'] ?? true),
540            'requirement' => $requirement?->value ?? IdentifyMethodRequirement::REQUIRED->value,
541            'signatureMethods' => $signatureMethods,
542            'signatureMethodEnabled' => $signatureMethodEnabled,
543        ];
544
545        return $defaultPolicyFactor;
546    }
547
548    /**
549     * Resolve UID from certificate chain data
550     *
551     * Extracts and resolves the identifier from certificate subject or extensions.
552     * Supports fallbacks for older LibreSign versions and converts to standard
553     * identifier format (account:uid or email:value).
554     *
555     * @param array $chainArr Certificate chain array with subject and extensions
556     * @param string $host Host domain for email matching
557     * @return string|null Resolved identifier in format "type:value" or null
558     */
559    public function resolveUid(array $chainArr, string $host): ?string {
560        if (!empty($chainArr['subject']['UID'])) {
561            return $chainArr['subject']['UID'];
562        }
563
564        return $this->subjectAlternativeNameService->resolveUid($chainArr, $host);
565    }
566}