Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.63% covered (warning)
72.63%
138 / 190
42.86% covered (danger)
42.86%
6 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SignFileController
72.63% covered (warning)
72.63%
138 / 190
42.86% covered (danger)
42.86%
6 / 14
72.80
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
 signByFileId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 signBySignerUuid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sign
84.38% covered (warning)
84.38%
54 / 64
0.00% covered (danger)
0.00%
0 / 1
20.38
 saveEmailSignerElements
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 signAsync
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
2
 signSync
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
2.01
 rejectByFileId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rejectBySignerUuid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 reject
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
6
 signRenew
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 requestCodeBySignerUuid
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
3.05
 requestCodeByFileId
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
2.06
 getCode
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
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\Controller;
10
11use OCA\Libresign\AppInfo\Application;
12use OCA\Libresign\Db\File;
13use OCA\Libresign\Db\SignRequest;
14use OCA\Libresign\Db\SignRequestMapper;
15use OCA\Libresign\Exception\LibresignException;
16use OCA\Libresign\Handler\SigningErrorHandler;
17use OCA\Libresign\Helper\JSActions;
18use OCA\Libresign\Middleware\Attribute\CanSignRequestUuid;
19use OCA\Libresign\Middleware\Attribute\RequireManager;
20use OCA\Libresign\Middleware\Attribute\RequireSigner;
21use OCA\Libresign\ResponseDefinitions;
22use OCA\Libresign\Service\AsyncSigningService;
23use OCA\Libresign\Service\EmailSignerElementsService;
24use OCA\Libresign\Service\File\SettingsLoader;
25use OCA\Libresign\Service\FileService;
26use OCA\Libresign\Service\IdentifyMethodService;
27use OCA\Libresign\Service\RequestMetadataService;
28use OCA\Libresign\Service\SignatureRejection\SignatureRejectionService;
29use OCA\Libresign\Service\SignerGeolocation\SignerGeolocationMetadataValidator;
30use OCA\Libresign\Service\SignerIpGeolocation\SignerIpGeolocationPolicyService;
31use OCA\Libresign\Service\SignFileService;
32use OCA\Libresign\Service\Validation\IdentityDocumentValidator;
33use OCA\Libresign\Service\Validation\SignerValidator;
34use OCA\Libresign\Service\Validation\SigningRequestValidator;
35use OCA\Libresign\Service\Validation\VisibleElementValidator;
36use OCA\Libresign\Service\Worker\WorkerConfiguration;
37use OCP\AppFramework\Http;
38use OCP\AppFramework\Http\Attribute\ApiRoute;
39use OCP\AppFramework\Http\Attribute\NoAdminRequired;
40use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
41use OCP\AppFramework\Http\Attribute\OpenAPI;
42use OCP\AppFramework\Http\Attribute\PublicPage;
43use OCP\AppFramework\Http\DataResponse;
44use OCP\IL10N;
45use OCP\IRequest;
46use OCP\IUser;
47use OCP\IUserSession;
48use Psr\Log\LoggerInterface;
49
50/**
51 * @psalm-import-type LibresignMessageResponse from ResponseDefinitions
52 * @psalm-import-type LibresignSignActionErrorResponse from ResponseDefinitions
53 * @psalm-import-type LibresignSignActionResponse from ResponseDefinitions
54 * @psalm-import-type LibresignSignatureRejectionResponse from ResponseDefinitions
55 * @psalm-import-type LibresignSignerDeviceGeolocation from ResponseDefinitions
56 */
57
58class SignFileController extends AEnvironmentAwareController implements ISignatureUuid {
59    use LibresignTrait;
60    public function __construct(
61        IRequest $request,
62        protected IL10N $l10n,
63        private SignRequestMapper $signRequestMapper,
64        protected IUserSession $userSession,
65        private IdentityDocumentValidator $identityDocumentValidator,
66        private VisibleElementValidator $visibleElementValidator,
67        private SignerValidator $signerValidator,
68        private SigningRequestValidator $signingRequestValidator,
69        protected SignFileService $signFileService,
70        private IdentifyMethodService $identifyMethodService,
71        private FileService $fileService,
72        private SettingsLoader $settingsLoader,
73        private WorkerConfiguration $workerConfiguration,
74        private AsyncSigningService $asyncSigningService,
75        private RequestMetadataService $requestMetadataService,
76        private SignerGeolocationMetadataValidator $signerGeolocationMetadataValidator,
77        private SignerIpGeolocationPolicyService $signerIpGeolocationPolicyService,
78        private SigningErrorHandler $errorHandler,
79        private SignatureRejectionService $signatureRejectionService,
80        private EmailSignerElementsService $emailSignerElementsService,
81        private LoggerInterface $logger,
82    ) {
83        parent::__construct(Application::APP_ID, $request);
84    }
85
86    /**
87     * Sign a file using file Id
88     *
89     * @param int $fileId Id of LibreSign file
90     * @param string $method Signature method
91     * @param array<string, mixed> $elements List of visible elements
92     * @param string $identifyValue Identify value
93     * @param string $token Token, commonly send by email
94     * @param bool $async Execute signing asynchronously when possible
95     * @param LibresignSignerDeviceGeolocation $deviceGeolocation Device-reported geolocation metadata submitted by the signing client
96     * @psalm-param array<string, mixed> $deviceGeolocation
97     * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignSignActionErrorResponse, array{}>
98     *
99     * 200: OK
100     * 404: Invalid data
101     * 422: Error
102     */
103    #[NoAdminRequired]
104    #[NoCSRFRequired]
105    #[RequireManager]
106    #[PublicPage]
107    #[OpenAPI(tags: ['signing'])]
108    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
109    public function signByFileId(int $fileId, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false, array $deviceGeolocation = []): DataResponse {
110        return $this->sign($method, $elements, $identifyValue, $token, $fileId, null, $async, $deviceGeolocation);
111    }
112
113    /**
114     * Sign a file using file UUID
115     *
116     * @param string $uuid UUID of LibreSign file
117     * @param string $method Signature method
118     * @param array<string, mixed> $elements List of visible elements
119     * @param string $identifyValue Identify value
120     * @param string $token Token, commonly send by email
121     * @param bool $async Execute signing asynchronously when possible
122     * @param LibresignSignerDeviceGeolocation $deviceGeolocation Device-reported geolocation metadata submitted by the signing client
123     * @param bool $saveSignature Explicitly save the external email signer's visible signature for future documents
124     * @psalm-param array<string, mixed> $deviceGeolocation
125     * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignSignActionErrorResponse, array{}>
126     *
127     * 200: OK
128     * 404: Invalid data
129     * 422: Error
130     */
131    #[NoAdminRequired]
132    #[NoCSRFRequired]
133    #[RequireSigner]
134    #[PublicPage]
135    #[OpenAPI(tags: ['signing'])]
136    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}', requirements: ['apiVersion' => '(v1)'])]
137    public function signBySignerUuid(string $uuid, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false, array $deviceGeolocation = [], bool $saveSignature = false): DataResponse {
138        return $this->sign($method, $elements, $identifyValue, $token, null, $uuid, $async, $deviceGeolocation, $saveSignature);
139    }
140
141    /**
142     * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignSignActionErrorResponse, array{}>
143     */
144    public function sign(
145        string $method,
146        array $elements = [],
147        string $identifyValue = '',
148        string $token = '',
149        ?int $fileId = null,
150        ?string $signRequestUuid = null,
151        bool $async = false,
152        array $deviceGeolocation = [],
153        bool $saveSignature = false,
154    ): DataResponse {
155        try {
156            $user = $this->userSession->getUser();
157            $isIdDocApproval = $this->request->getParam('idDocApproval') === 'true';
158
159            if ($isIdDocApproval && $signRequestUuid) {
160                $libreSignFile = $this->signFileService->getFileByUuid($signRequestUuid);
161                $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, null, $user);
162            } else {
163                $libreSignFile = $this->signFileService->getLibresignFile($fileId, $signRequestUuid);
164                $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, $signRequestUuid, $user);
165            }
166
167            $this->identityDocumentValidator->canSignWithIdentificationDocumentStatus(
168                $user,
169                $this->settingsLoader->getIdentificationDocumentsStatus($user, $signRequest)
170            );
171
172            $this->visibleElementValidator->validateVisibleElementsRelation($elements, $signRequest, $user);
173            $this->signerValidator->validateCredentials($signRequest, $method, $identifyValue, $token);
174
175            $userIdentifier = $this->identifyMethodService->getUserIdentifier($signRequest->getId());
176            $metadata = $this->requestMetadataService->collectMetadata();
177            $normalizedDeviceGeolocation = $this->signerGeolocationMetadataValidator->normalize(
178                $deviceGeolocation === [] ? null : $deviceGeolocation,
179            );
180            $this->signerGeolocationMetadataValidator->validateSubmission($signRequest, $normalizedDeviceGeolocation);
181            if ($normalizedDeviceGeolocation !== null) {
182                $metadata = $this->signerGeolocationMetadataValidator->mergeDeviceIntoMetadata(
183                    $metadata,
184                    $normalizedDeviceGeolocation,
185                );
186            }
187
188            // GeoIP must run before the async/sync split so background workers never
189            // resolve location from a non-signer connection.
190            $ipGeolocation = $this->signerIpGeolocationPolicyService->collectMetadata(
191                $libreSignFile,
192                $this->request,
193            );
194            if ($ipGeolocation !== null) {
195                $metadata = $this->signerIpGeolocationPolicyService->mergeIntoMetadata(
196                    $metadata,
197                    $ipGeolocation,
198                );
199            }
200
201            $signature = null;
202            $signatureSaveFailed = false;
203            if ($saveSignature && !$user instanceof IUser) {
204                try {
205                    $signature = $this->emailSignerElementsService->prepareSignature($signRequest, $token, $elements, true);
206                    $signatureSaveFailed = $signature === null;
207                } catch (\Throwable $e) {
208                    $this->logger->warning('Unable to prepare signature elements of email signer', ['exception' => $e]);
209                    $signatureSaveFailed = true;
210                }
211            }
212
213            $this->signFileService->prepareForSigning(
214                $libreSignFile,
215                $signRequest,
216                $user,
217                $userIdentifier,
218                $signRequest->getDisplayName(),
219                $method !== 'password',
220                $method === 'password' ? $token : null,
221                $method,
222            );
223
224            if ($async && $this->workerConfiguration->isAsyncSigningEnabled()) {
225                $response = $this->signAsync($libreSignFile, $signRequest, $user, $userIdentifier, $method, $token, $elements, $metadata);
226            } else {
227                $response = $this->signSync($libreSignFile, $elements, $metadata);
228            }
229            if ($response->getStatus() === Http::STATUS_OK && $saveSignature && !$user instanceof IUser) {
230                if ($signature !== null && !$this->saveEmailSignerElements($signature)) {
231                    $signatureSaveFailed = true;
232                }
233                $data = $response->getData();
234                $data['signatureSaveStatus'] = $signatureSaveFailed ? 'failed' : 'saved';
235                $response->setData($data);
236            }
237            return $response;
238        } catch (\Throwable $e) {
239            $data = $this->errorHandler->handleException($e);
240            return new DataResponse($data, Http::STATUS_UNPROCESSABLE_ENTITY);
241        }
242    }
243
244    /** @param array{key: string, elements: array<string, string>} $signature */
245    private function saveEmailSignerElements(array $signature): bool {
246        try {
247            $this->emailSignerElementsService->saveSignature($signature);
248            return true;
249        } catch (\Throwable $e) {
250            // A failed optional save must never undo successful document signing.
251            $this->logger->warning('Unable to save signature elements of email signer', ['exception' => $e]);
252            return false;
253        }
254    }
255
256    /**
257     * Execute asynchronous signing using background job
258     *
259     * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>
260     */
261    private function signAsync(
262        File $libreSignFile,
263        SignRequest $signRequest,
264        ?IUser $user,
265        string $userIdentifier,
266        string $method,
267        ?string $token,
268        array $elements,
269        array $metadata,
270    ): DataResponse {
271        $this->signFileService->validateSigningRequirements();
272
273        $this->asyncSigningService->enqueueSigningJob(
274            $libreSignFile,
275            $signRequest,
276            $user,
277            $userIdentifier,
278            $method !== 'password',
279            $method === 'password' ? $token : null,
280            $method,
281            $elements,
282            $metadata,
283        );
284
285        return new DataResponse(
286            [
287                'action' => JSActions::ACTION_DO_NOTHING,
288                'job' => [
289                    'status' => 'SIGNING_IN_PROGRESS',
290                    'file' => [
291                        'uuid' => $libreSignFile->getUuid(),
292                    ],
293                ],
294            ],
295            Http::STATUS_OK
296        );
297    }
298
299    /**
300     * Execute synchronous signing immediately
301     *
302     * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>
303     */
304    private function signSync($libreSignFile, array $elements, array $metadata): DataResponse {
305        $this->signFileService
306            ->setVisibleElements($elements)
307            ->storeUserMetadata($metadata)
308            ->sign();
309
310        $validationUuid = $libreSignFile->getUuid();
311        if ($libreSignFile->hasParent()) {
312            $parentFile = $this->signFileService->getFile($libreSignFile->getParentFileId());
313            $validationUuid = $parentFile->getUuid();
314        }
315
316        return new DataResponse(
317            [
318                'action' => JSActions::ACTION_SIGNED,
319                // TRANSLATORS Success message shown to the signer after the digital signature is applied to the document.
320                'message' => $this->l10n->t('File signed'),
321                'file' => [
322                    'uuid' => $validationUuid
323                ]
324            ],
325            Http::STATUS_OK
326        );
327    }
328
329    /**
330     * Reject a signature request using file Id
331     *
332     * @param int $fileId Id of LibreSign file
333     * @param string $comment Justification sent by the signer, when the document policy accepts comments
334     * @param bool $privateComment Keep the comment visible only to who requested the signature
335     * @return DataResponse<Http::STATUS_OK, LibresignSignatureRejectionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
336     *
337     * 200: OK
338     * 422: Error
339     */
340    #[NoAdminRequired]
341    #[NoCSRFRequired]
342    #[OpenAPI(tags: ['signing'])]
343    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}/reject', requirements: ['apiVersion' => '(v1)'])]
344    public function rejectByFileId(int $fileId, string $comment = '', bool $privateComment = false): DataResponse {
345        return $this->reject($comment, $privateComment, $fileId, null);
346    }
347
348    /**
349     * Reject a signature request using the signer UUID
350     *
351     * @param string $uuid UUID of the signer
352     * @param string $comment Justification sent by the signer, when the document policy accepts comments
353     * @param bool $privateComment Keep the comment visible only to who requested the signature
354     * @return DataResponse<Http::STATUS_OK, LibresignSignatureRejectionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
355     *
356     * 200: OK
357     * 422: Error
358     */
359    #[NoAdminRequired]
360    #[NoCSRFRequired]
361    #[RequireSigner]
362    #[PublicPage]
363    #[OpenAPI(tags: ['signing'])]
364    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}/reject', requirements: ['apiVersion' => '(v1)'])]
365    public function rejectBySignerUuid(string $uuid, string $comment = '', bool $privateComment = false): DataResponse {
366        return $this->reject($comment, $privateComment, null, $uuid);
367    }
368
369    /**
370     * @return DataResponse<Http::STATUS_OK, LibresignSignatureRejectionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
371     */
372    private function reject(
373        string $comment,
374        bool $privateComment,
375        ?int $fileId,
376        ?string $signRequestUuid,
377    ): DataResponse {
378        try {
379            $user = $this->userSession->getUser();
380            $libreSignFile = $this->signFileService->getLibresignFile($fileId, $signRequestUuid);
381            $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, $signRequestUuid, $user);
382
383            $signRequest = $this->signatureRejectionService->reject(
384                $libreSignFile,
385                $signRequest,
386                $comment,
387                $privateComment,
388            );
389        } catch (\Throwable $th) {
390            return new DataResponse(
391                ['message' => $th->getMessage()],
392                Http::STATUS_UNPROCESSABLE_ENTITY,
393            );
394        }
395
396        return new DataResponse(
397            [
398                // TRANSLATORS Success message shown to the signer after refusing to sign the document.
399                'message' => $this->l10n->t('Signature request rejected.'),
400                'signRequestId' => (int)$signRequest->getId(),
401                'status' => $signRequest->getStatus(),
402                'statusText' => $this->signRequestMapper->getTextOfSignerStatus($signRequest->getStatus()),
403                'rejectedAt' => (string)$signRequest->getRejectedAt()?->format(\DateTimeInterface::ATOM),
404                'workflowCanceled' => $this->signatureRejectionService->isWorkflowCanceled($libreSignFile),
405            ],
406            Http::STATUS_OK,
407        );
408    }
409
410    /**
411     * Renew the signature method
412     *
413     * @param string $method Signature method
414     * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>
415     *
416     * 200: OK
417     */
418    #[NoAdminRequired]
419    #[NoCSRFRequired]
420    #[PublicPage]
421    #[CanSignRequestUuid]
422    #[OpenAPI(tags: ['signing'])]
423    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}/renew/{method}', requirements: ['apiVersion' => '(v1)'])]
424    public function signRenew(string $method): DataResponse {
425        $this->signFileService->renew(
426            $this->getSignRequestEntity(),
427            $method,
428        );
429        return new DataResponse(
430            [
431                // TRANSLATORS Message sent to signer when the sign link was expired and was possible to request to renew. The signer will see this message on the screen and nothing more.
432                'message' => $this->l10n->t('Renewed with success. Access the link again.'),
433            ]
434        );
435    }
436
437    /**
438     * Get code to sign the document using UUID
439     *
440     * @param string $uuid UUID of LibreSign file
441     * @param 'account'|'email'|null $identifyMethod Identify signer method
442     * @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, smsToken, signalToken, telegramToken, whatsappToken, xmppToken
443     * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
444     *
445     * 200: OK
446     * 422: Error
447     */
448    #[NoAdminRequired]
449    #[NoCSRFRequired]
450    #[RequireSigner]
451    #[PublicPage]
452    #[OpenAPI(tags: ['signing'])]
453    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}/code', requirements: ['apiVersion' => '(v1)'])]
454    public function requestCodeBySignerUuid(string $uuid, ?string $identifyMethod, ?string $signMethod): DataResponse {
455        try {
456            if ($this->request->getParam('idDocApproval') === 'true') {
457                // In this context the uuid is the one of the file, not of a sign request.
458                $libreSignFile = $this->signFileService->getFileByUuid($uuid);
459                $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, null, $this->userSession->getUser());
460            } else {
461                $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId($uuid);
462            }
463        } catch (\Throwable) {
464            // TRANSLATORS Error shown when the data required to apply a digital signature is missing or invalid.
465            throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
466        }
467        return $this->getCode(
468            $signRequest,
469            $identifyMethod ?? '',
470            $signMethod ?? '',
471        );
472    }
473
474    /**
475     * Get code to sign the document using FileID
476     *
477     * @param int $fileId Id of LibreSign file
478     * @param 'account'|'email'|null $identifyMethod Identify signer method
479     * @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, smsToken, signalToken, telegramToken, whatsappToken, xmppToken
480     * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
481     *
482     * 200: OK
483     * 422: Error
484     */
485    #[NoAdminRequired]
486    #[NoCSRFRequired]
487    #[RequireSigner]
488    #[PublicPage]
489    #[OpenAPI(tags: ['signing'])]
490    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}/code', requirements: ['apiVersion' => '(v1)'])]
491    public function requestCodeByFileId(int $fileId, ?string $identifyMethod, ?string $signMethod): DataResponse {
492        try {
493            $signRequest = $this->signRequestMapper->getByFileIdAndUserId($fileId);
494        } catch (\Throwable) {
495            // TRANSLATORS Error shown when the data required to apply a digital signature is missing or invalid.
496            throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
497        }
498        return $this->getCode(
499            $signRequest,
500            $identifyMethod ?? '',
501            $signMethod ?? '',
502        );
503    }
504
505    /**
506     * @todo validate if can request code
507     * @return DataResponse<Http::STATUS_OK|Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
508     */
509    private function getCode(
510        SignRequest $signRequest,
511        string $identifyMethodName,
512        string $signMethodName,
513    ): DataResponse {
514        try {
515            $libreSignFile = $this->signFileService->getFile($signRequest->getFileId());
516            $this->signingRequestValidator->fileCanBeSigned($libreSignFile);
517            $this->signFileService->requestCode(
518                signRequest: $signRequest,
519                identifyMethodName: $identifyMethodName,
520                signMethodName: $signMethodName,
521            );
522            // TRANSLATORS Success message shown after sending a one-time verification code used to confirm the signer identity before signing.
523            $message = $this->l10n->t('Verification code sent.');
524            $statusCode = Http::STATUS_OK;
525        } catch (\Throwable $th) {
526            $this->logger->error('Unable to send verification code.', [
527                'exception' => $th,
528            ]);
529            // TRANSLATORS Generic error shown when a verification code cannot be delivered.
530            $message = $this->l10n->t('Unable to send verification code.');
531            $statusCode = Http::STATUS_UNPROCESSABLE_ENTITY;
532        }
533        return new DataResponse(
534            [
535                'message' => $message,
536            ],
537            $statusCode,
538        );
539    }
540}