Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.74% covered (warning)
78.74%
274 / 348
33.33% covered (danger)
33.33%
8 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestSignatureService
78.74% covered (warning)
78.74%
274 / 348
33.33% covered (danger)
33.33%
8 / 24
211.01
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
 saveFiles
94.59% covered (success)
94.59%
35 / 37
0.00% covered (danger)
0.00%
0 / 1
5.00
 save
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 propagateSignersToChildren
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
26.83
 saveEnvelope
78.05% covered (warning)
78.05%
32 / 41
0.00% covered (danger)
0.00%
0 / 1
6.38
 processFileData
66.67% covered (warning)
66.67%
14 / 21
0.00% covered (danger)
0.00%
0 / 1
3.33
 requireFileName
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 rollbackEnvelopeCreation
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 rollbackCreatedNodes
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 rollbackCreatedFiles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 rollbackEnvelope
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 createFileForEnvelope
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
2.00
 saveFile
78.00% covered (warning)
78.00%
39 / 50
0.00% covered (danger)
0.00%
0 / 1
22.84
 getFileMetadata
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 removeExtensionFromName
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 deleteIdentifyMethodIfNotExits
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
6.17
 identifyMethodExists
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 associateToSigners
98.11% covered (success)
98.11%
52 / 53
0.00% covered (danger)
0.00%
0 / 1
11
 saveVisibleElements
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
9
 validateNewRequestToFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 validateNewFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 validateSigners
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 unassociateToUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteRequestSignature
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
4.25
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\File as FileEntity;
12use OCA\Libresign\Db\FileElementMapper;
13use OCA\Libresign\Db\FileMapper;
14use OCA\Libresign\Db\IdentifyMethodMapper;
15use OCA\Libresign\Db\SignRequest as SignRequestEntity;
16use OCA\Libresign\Db\SignRequestMapper;
17use OCA\Libresign\Enum\FileStatus;
18use OCA\Libresign\Enum\ParticipantRole;
19use OCA\Libresign\Exception\LibresignException;
20use OCA\Libresign\Handler\DocMdpHandler;
21use OCA\Libresign\Helper\FileUploadHelper;
22use OCA\Libresign\Service\DocMdp\ConfigService as DocMdpConfigService;
23use OCA\Libresign\Service\Envelope\EnvelopeFileRelocator;
24use OCA\Libresign\Service\Envelope\EnvelopeService;
25use OCA\Libresign\Service\File\Pdf\PdfMetadataExtractor;
26use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
27use OCA\Libresign\Service\Policy\FilePolicyApplier;
28use OCA\Libresign\Service\SignerGeolocation\SignerGeolocationPolicyService;
29use OCA\Libresign\Service\SignRequest\SignRequestService;
30use OCA\Libresign\Service\Validation\FileInputValidator;
31use OCA\Libresign\Service\Validation\SignerValidator;
32use OCA\Libresign\Service\Validation\SigningRequestValidator;
33use OCP\AppFramework\Db\DoesNotExistException;
34use OCP\AppFramework\Db\MultipleObjectsReturnedException;
35use OCP\AppFramework\Db\TTransactional;
36use OCP\Files\IMimeTypeDetector;
37use OCP\Files\Node;
38use OCP\Http\Client\IClientService;
39use OCP\IAppConfig;
40use OCP\IDBConnection;
41use OCP\IL10N;
42use OCP\IUser;
43use OCP\IUserManager;
44use Psr\Log\LoggerInterface;
45use Sabre\DAV\UUIDUtil;
46
47class RequestSignatureService {
48    use TTransactional;
49
50    public function __construct(
51        protected FileService $fileService,
52        protected IL10N $l10n,
53        protected IdentifyMethodService $identifyMethod,
54        protected SignRequestMapper $signRequestMapper,
55        protected IUserManager $userManager,
56        protected FileMapper $fileMapper,
57        protected IdentifyMethodMapper $identifyMethodMapper,
58        protected PdfMetadataExtractor $pdfMetadataExtractor,
59        protected FileElementService $fileElementService,
60        protected FileElementMapper $fileElementMapper,
61        protected FolderService $folderService,
62        protected IMimeTypeDetector $mimeTypeDetector,
63        protected FileInputValidator $fileInputValidator,
64        protected SigningRequestValidator $signingRequestValidator,
65        protected SignerValidator $signerValidator,
66        protected IClientService $client,
67        protected DocMdpHandler $docMdpHandler,
68        protected LoggerInterface $logger,
69        protected SequentialSigningService $sequentialSigningService,
70        protected IAppConfig $appConfig,
71        protected FileStatusService $fileStatusService,
72        protected DocMdpConfigService $docMdpConfigService,
73        protected EnvelopeService $envelopeService,
74        protected EnvelopeFileRelocator $envelopeFileRelocator,
75        protected FileUploadHelper $uploadHelper,
76        protected SignRequestService $signRequestService,
77        protected FilePolicyApplier $filePolicyApplier,
78        protected SignerGeolocationPolicyService $signerGeolocationPolicyService,
79        protected SignerRemovalService $signerRemovalService,
80        protected IDBConnection $db,
81        protected NotificationDeliveryService $notifications,
82    ) {
83    }
84
85    /**
86     * Save files - creates single file or envelope based on files count
87     *
88     * @return array{file: FileEntity, children: list<FileEntity>}
89     */
90    public function saveFiles(array $data): array {
91        if (empty($data['files'])) {
92            throw new LibresignException('Files parameter is required');
93        }
94
95        if (count($data['files']) === 1) {
96            $fileData = $data['files'][0];
97
98            $saveData = [
99                'name' => $data['name'] ?? $fileData['name'] ?? '',
100                'userManager' => $data['userManager'],
101                'status' => FileStatus::DRAFT->value,
102                'settings' => $data['settings'],
103                'policyOverrides' => $data['policyOverrides'] ?? [],
104                'policyActiveContext' => $data['policyActiveContext'] ?? null,
105            ];
106
107            if (isset($fileData['uploadedFile'])) {
108                $saveData['uploadedFile'] = $fileData['uploadedFile'];
109            } elseif (isset($fileData['fileNode'])) {
110                $saveData['file'] = ['fileNode' => $fileData['fileNode']];
111            } else {
112                $saveData['file'] = $fileData;
113            }
114
115            $savedFile = $this->save($saveData);
116
117            return [
118                'file' => $savedFile,
119                'children' => [$savedFile],
120            ];
121        }
122
123        $result = $this->saveEnvelope([
124            'files' => $data['files'],
125            'name' => $data['name'],
126            'userManager' => $data['userManager'],
127            'settings' => $data['settings'],
128            'signers' => $data['signers'] ?? [],
129            'status' => $data['status'] ?? FileStatus::DRAFT->value,
130            'visibleElements' => $data['visibleElements'] ?? [],
131            'policyOverrides' => $data['policyOverrides'] ?? [],
132            'policyActiveContext' => $data['policyActiveContext'] ?? null,
133        ]);
134
135        return [
136            'file' => $result['envelope'],
137            'children' => $result['files'],
138        ];
139    }
140
141    public function save(array $data): FileEntity {
142        $file = $this->saveFile($data);
143        if (!isset($data['status'])) {
144            $data['status'] = $file->getStatus();
145        }
146        $this->atomic(function () use ($file, $data): void {
147            $this->sequentialSigningService->setFile($file);
148            $this->associateToSigners($data, $file);
149            $this->propagateSignersToChildren($file, $data);
150            $this->saveVisibleElements($data, $file);
151        }, $this->db);
152        $this->notifications->flushCommitted();
153
154        return $file;
155    }
156
157    private function propagateSignersToChildren(FileEntity $envelope, array $data): void {
158        if ($envelope->getNodeType() !== 'envelope' || empty($data['signers'])) {
159            return;
160        }
161
162        $children = $this->fileMapper->getChildrenFiles($envelope->getId());
163
164        $dataWithoutNotification = $data;
165        foreach ($dataWithoutNotification['signers'] as &$signer) {
166            $signer['notify'] = 0;
167        }
168
169        foreach ($children as $child) {
170            $this->identifyMethod->clearCache();
171            $this->sequentialSigningService->setFile($child);
172            $this->associateToSigners($dataWithoutNotification, $child);
173        }
174
175        if ($envelope->getStatus() > FileStatus::DRAFT->value) {
176            $this->fileStatusService->propagateStatusToChildren($envelope->getId(), $envelope->getStatus());
177        }
178    }
179
180    public function saveEnvelope(array $data): array {
181        $this->envelopeService->validateEnvelopeConstraints(count($data['files']));
182
183        // TRANSLATORS Default name for a new multi-document envelope when the requester did not provide one. %s is the creation date and time.
184        $envelopeName = $data['name'] ?: $this->l10n->t('Envelope %s', [date('Y-m-d H:i:s')]);
185        $userManager = $data['userManager'] ?? null;
186        $userId = $userManager instanceof IUser ? $userManager->getUID() : null;
187        $filesCount = count($data['files']);
188
189        $envelope = null;
190        $files = [];
191        $createdNodes = [];
192
193        try {
194            $envelopePath = $data['settings']['path'] ?? null;
195            $envelope = $this->envelopeService->createEnvelope($envelopeName, $userId, $filesCount, $envelopePath, $data);
196
197            $envelopeFolder = $this->envelopeService->getEnvelopeFolder($envelope);
198            $envelopeSettings = array_merge($data['settings'] ?? [], [
199                'envelopeFolderId' => $envelopeFolder->getId(),
200            ]);
201
202            foreach ($data['files'] as $fileData) {
203                $node = $this->processFileData($fileData, $userManager, $envelopeSettings);
204                $createdNodes[] = $node;
205
206                $fileData['node'] = $node;
207                $fileEntity = $this->createFileForEnvelope(
208                    $fileData,
209                    $userManager,
210                    $envelopeSettings,
211                    $data['policyOverrides'] ?? [],
212                    $data['policyActiveContext'] ?? null,
213                );
214                $this->envelopeService->addFileToEnvelope($envelope->getId(), $fileEntity);
215                $files[] = $fileEntity;
216            }
217
218            if (!empty($data['signers'])) {
219                $this->atomic(function () use ($envelope, $data): void {
220                    $this->sequentialSigningService->setFile($envelope);
221                    $this->associateToSigners($data, $envelope);
222                    $this->propagateSignersToChildren($envelope, $data);
223                }, $this->db);
224                $this->notifications->flushCommitted();
225            }
226
227            return [
228                'envelope' => $envelope,
229                'files' => $files,
230            ];
231        } catch (\Throwable $e) {
232            $this->rollbackEnvelopeCreation($envelope, $files, $createdNodes);
233            throw $e;
234        }
235    }
236
237    private function processFileData(array $fileData, ?IUser $userManager, array $settings): Node {
238        $name = $this->requireFileName($fileData);
239
240        if (isset($fileData['uploadedFile'])) {
241            $sourceNode = $this->fileService->getNodeFromData([
242                'userManager' => $userManager,
243                'name' => $name,
244                'uploadedFile' => $fileData['uploadedFile'],
245                'settings' => $settings,
246            ]);
247        } else {
248            $sourceNode = $this->fileService->getNodeFromData([
249                'userManager' => $userManager,
250                'name' => $name,
251                'file' => $fileData['file'] ?? $fileData,
252                'settings' => $settings,
253            ]);
254        }
255
256        if (isset($settings['envelopeFolderId'])) {
257            return $this->envelopeFileRelocator->ensureFileInEnvelopeFolder(
258                $sourceNode,
259                $settings['envelopeFolderId'],
260                $userManager,
261            );
262        }
263
264        return $sourceNode;
265    }
266
267    private function requireFileName(array $fileData): string {
268        $name = trim((string)($fileData['name'] ?? ''));
269        if ($name === '') {
270            // TRANSLATORS Error shown when creating a signature request without a document file name.
271            throw new LibresignException($this->l10n->t('File name is required'));
272        }
273        return $name;
274    }
275
276    private function rollbackEnvelopeCreation(?FileEntity $envelope, array $files, array $createdNodes): void {
277        $this->rollbackCreatedNodes($createdNodes);
278        $this->rollbackCreatedFiles($files);
279        $this->rollbackEnvelope($envelope);
280    }
281
282    private function rollbackCreatedNodes(array $nodes): void {
283        foreach ($nodes as $node) {
284            try {
285                $node->delete();
286            } catch (\Throwable $deleteError) {
287                $this->logger->error('Failed to rollback created node in envelope', [
288                    'nodeId' => $node->getId(),
289                    'error' => $deleteError->getMessage(),
290                ]);
291            }
292        }
293    }
294
295    private function rollbackCreatedFiles(array $files): void {
296        foreach ($files as $file) {
297            try {
298                $this->fileMapper->delete($file);
299            } catch (\Throwable $deleteError) {
300                $this->logger->error('Failed to rollback created file entity in envelope', [
301                    'fileId' => $file->getId(),
302                    'error' => $deleteError->getMessage(),
303                ]);
304            }
305        }
306    }
307
308    private function rollbackEnvelope(?FileEntity $envelope): void {
309        if ($envelope === null) {
310            return;
311        }
312
313        try {
314            $this->fileMapper->delete($envelope);
315        } catch (\Throwable $deleteError) {
316            $this->logger->error('Failed to rollback created envelope', [
317                'envelopeId' => $envelope->getId(),
318                'error' => $deleteError->getMessage(),
319            ]);
320        }
321    }
322
323    private function createFileForEnvelope(
324        array $fileData,
325        ?IUser $userManager,
326        array $settings,
327        array $policyOverrides = [],
328        ?array $policyActiveContext = null,
329    ): FileEntity {
330        if (!isset($fileData['node'])) {
331            throw new \InvalidArgumentException('Node not provided in file data');
332        }
333
334        $node = $fileData['node'];
335        $fileName = $fileData['name'] ?? $node->getName();
336
337        return $this->saveFile([
338            'file' => ['fileNode' => $node],
339            'name' => $fileName,
340            'userManager' => $userManager,
341            'status' => FileStatus::DRAFT->value,
342            'settings' => $settings,
343            'policyOverrides' => $policyOverrides,
344            'policyActiveContext' => $policyActiveContext,
345        ]);
346    }
347
348    /**
349     * Save file data
350     *
351     * @param array{
352     *     userManager?: IUser,
353     *     signRequest?: SignRequestEntity,
354     *     name?: string,
355     *     callback?: string,
356     *     uuid?: ?string,
357     *     status?: int,
358     *     parentFileId?: int,
359     *     file?: array{fileId?: int, nodeId?: int, fileNode?: Node},
360     *     settings?: array<string, mixed>,
361     *     policyOverrides?: array<string, mixed>,
362     *     policyActiveContext?: ?array<string, mixed>,
363     *     uploadedFile?: array<string, mixed>
364     * } $data
365     */
366    public function saveFile(array $data): FileEntity {
367        if (!empty($data['uuid'])) {
368            $file = $this->fileMapper->getByUuid($data['uuid']);
369            $this->filePolicyApplier->syncCoreFlowPolicies($file, $data);
370            if (!empty($data['name'])) {
371                $file->setName($data['name']);
372                $this->fileService->update($file);
373            }
374            return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
375        }
376        $nodeId = null;
377        if (isset($data['file']['fileNode']) && $data['file']['fileNode'] instanceof Node) {
378            $nodeId = $data['file']['fileNode']->getId();
379        } elseif (!empty($data['file']['nodeId'])) {
380            $nodeId = $data['file']['nodeId'];
381        }
382        if (!is_null($nodeId)) {
383            try {
384                $file = $this->fileMapper->getByNodeId($nodeId);
385            } catch (DoesNotExistException) {
386                $file = null;
387            } catch (MultipleObjectsReturnedException) {
388                // TRANSLATORS Error shown when a document cannot be updated because LibreSign has more than one record for it.
389                throw new LibresignException($this->l10n->t('This document is registered more than once, so it cannot be updated. Please contact your administrator.'), 422);
390            }
391            if ($file !== null) {
392                $this->filePolicyApplier->syncAllPolicies($file, $data);
393                return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
394            }
395        }
396
397        $node = $this->fileService->getNodeFromData($data);
398
399        $file = new FileEntity();
400        $file->setNodeId($node->getId());
401        if (isset($data['userManager']) && $data['userManager'] instanceof IUser) {
402            $file->setUserId($data['userManager']->getUID());
403        } elseif (isset($data['signRequest']) && $data['signRequest'] instanceof SignRequestEntity) {
404            $signRequestFileId = $data['signRequest']->getFileId();
405            if ($signRequestFileId) {
406                $signRequestFile = $this->fileMapper->getById($signRequestFileId);
407                $file->setUserId($signRequestFile->getUserId());
408            }
409        }
410        $file->setUuid(UUIDUtil::getUUID());
411        $file->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
412        $metadata = $this->getFileMetadata($node);
413        $name = trim((string)($data['name'] ?? ''));
414        if ($name === '') {
415            $name = $node->getName();
416        }
417        $file->setName($this->removeExtensionFromName($name, $metadata));
418        $file->setMetadata($metadata);
419        if (!empty($data['callback'])) {
420            $file->setCallback($data['callback']);
421        }
422        if (isset($data['status'])) {
423            $file->setStatus($data['status']);
424        } else {
425            $file->setStatus(FileStatus::ABLE_TO_SIGN->value);
426        }
427        $this->fileStatusService->markPolicySnapshotFrozen($file, $file->getStatus());
428
429        if (isset($data['parentFileId'])) {
430            $file->setParentFileId($data['parentFileId']);
431        }
432
433        $this->filePolicyApplier->applyAll($file, $data);
434
435        $this->fileMapper->insert($file);
436        return $file;
437    }
438
439    private function getFileMetadata(\OCP\Files\Node $node): array {
440        $metadata = [];
441        if ($extension = strtolower($node->getExtension())) {
442            $metadata = [
443                'extension' => $extension,
444            ];
445            if ($metadata['extension'] === 'pdf') {
446                $this->pdfMetadataExtractor->setFile($node);
447                $metadata = array_merge(
448                    $metadata,
449                    $this->pdfMetadataExtractor->getPageDimensions()
450                );
451                $metadata['pdfVersion'] = $this->pdfMetadataExtractor->getPdfVersion();
452            }
453        }
454        return $metadata;
455    }
456
457    private function removeExtensionFromName(string $name, array $metadata): string {
458        if (!isset($metadata['extension'])) {
459            return $name;
460        }
461        $extensionPattern = '/\.' . preg_quote($metadata['extension'], '/') . '$/i';
462        $result = preg_replace($extensionPattern, '', $name);
463        return $result ?? $name;
464    }
465
466    private function deleteIdentifyMethodIfNotExits(array $signers, FileEntity $file): void {
467        $normalizedSigners = $this->signerValidator->normalizeRequestSigners($signers);
468        $signRequests = $this->signRequestMapper->getByFileId($file->getId());
469        foreach ($signRequests as $key => $signRequest) {
470            $identifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequest->getId());
471            if (empty($identifyMethods)) {
472                $this->unassociateToUser($file->getId(), $signRequest->getId());
473                continue;
474            }
475            foreach ($identifyMethods as $methodName => $list) {
476                foreach ($list as $method) {
477                    if (!$this->identifyMethodExists($normalizedSigners, $method)) {
478                        $this->unassociateToUser($file->getId(), $signRequest->getId());
479                        continue 3;
480                    }
481                }
482            }
483        }
484    }
485
486    private function identifyMethodExists(array $signers, IIdentifyMethod $identifyMethod): bool {
487        foreach ($signers as $signer) {
488            foreach (($signer['identifyMethods'] ?? []) as $data) {
489                if ($identifyMethod->getEntity()->getIdentifierKey() !== $data['method']) {
490                    continue;
491                }
492                if ($identifyMethod->getEntity()->getIdentifierValue() === $data['value']) {
493                    return true;
494                }
495            }
496        }
497        return false;
498    }
499
500    /**
501     * @return SignRequestEntity[]
502     *
503     * @psalm-return list<SignRequestEntity>
504     */
505    private function associateToSigners(array $data, FileEntity $file): array {
506        $return = [];
507        if (!empty($data['signers'])) {
508            $normalizedSigners = $this->signerValidator->normalizeRequestSigners($data['signers']);
509            $this->deleteIdentifyMethodIfNotExits($normalizedSigners, $file);
510            $this->identifyMethod->clearCache();
511
512            $this->sequentialSigningService->resetOrderCounter();
513            $fileStatus = $data['status'] ?? null;
514
515            foreach ($normalizedSigners as $signer) {
516                $participantRole = ParticipantRole::fromNullable($signer['participantRole'] ?? null);
517                $userProvidedOrder = isset($signer['signingOrder']) ? (int)$signer['signingOrder'] : null;
518                $signingOrder = $participantRole->canSign()
519                    ? $this->sequentialSigningService->determineSigningOrder($userProvidedOrder)
520                    : 0;
521                $signerStatus = $signer['status'] ?? null;
522                $shouldNotify = !isset($signer['notify']) || $signer['notify'] !== 0;
523                // One logical signer write can contain multiple identify methods.
524                // Each explicit write gets a distinct event, shared by its methods.
525                $deliveryEventId = $shouldNotify ? 'sign-request-write:' . bin2hex(random_bytes(16)) : null;
526                $lastSignRequest = null;
527
528                // Absent key means "leave frozen requirement unchanged" on updates.
529                // Only default to false when creating a sign request that has no freeze yet.
530                $requesterRequiresGeolocation = null;
531                if (array_key_exists('deviceGeolocationRequired', $signer)) {
532                    $requesterRequiresGeolocation = filter_var(
533                        $signer['deviceGeolocationRequired'],
534                        FILTER_VALIDATE_BOOLEAN,
535                        FILTER_NULL_ON_FAILURE,
536                    ) ?? false;
537                }
538
539                foreach ($signer['identifyMethods'] as $identifyMethod) {
540                    $lastSignRequest = $this->signRequestService->createOrUpdateSignRequest(
541                        identifyMethods: [
542                            $identifyMethod['method'] => $identifyMethod['value'],
543                        ],
544                        displayName: $signer['displayName'] ?? '',
545                        description: $signer['description'] ?? null,
546                        notify: $shouldNotify,
547                        fileId: $file->getId(),
548                        signingOrder: $signingOrder,
549                        fileStatus: $fileStatus,
550                        signerStatus: $signerStatus,
551                        participantRole: $participantRole,
552                        afterPersist: function (SignRequestEntity $signRequest) use ($file, $requesterRequiresGeolocation): void {
553                            $requiresGeolocation = $requesterRequiresGeolocation;
554                            if ($requiresGeolocation === null) {
555                                if ($this->signerGeolocationPolicyService->getFrozenRequirement($signRequest) !== null) {
556                                    return;
557                                }
558                                $requiresGeolocation = false;
559                            }
560                            $this->signerGeolocationPolicyService->persistEffectiveRequirement(
561                                $signRequest,
562                                $file,
563                                $requiresGeolocation,
564                            );
565                        },
566                        deliveryEventId: $deliveryEventId,
567                    );
568                    $return[] = $lastSignRequest;
569                }
570            }
571        }
572        return $return;
573    }
574
575    private function saveVisibleElements(array $data, FileEntity $file): array {
576        if (empty($data['visibleElements'])) {
577            return [];
578        }
579        $persisted = [];
580        foreach ($data['visibleElements'] as $element) {
581            if ($file->isEnvelope() && !empty($element['signRequestId'])) {
582                $envelopeSignRequest = $this->signRequestMapper->getById((int)$element['signRequestId']);
583                // Only translate if the provided SR belongs to the envelope itself
584                if ($envelopeSignRequest && $envelopeSignRequest->getFileId() === $file->getId()) {
585                    $childrenSrs = $this->signRequestMapper->getByEnvelopeChildrenAndIdentifyMethod($file->getId(), (int)$element['signRequestId']);
586                    foreach ($childrenSrs as $childSr) {
587                        if ($childSr->getFileId() === (int)$element['fileId']) {
588                            $element['signRequestId'] = $childSr->getId();
589                            break;
590                        }
591                    }
592                }
593            }
594
595            $persisted[] = $this->fileElementService->saveVisibleElement($element);
596        }
597        return $persisted;
598    }
599
600    public function validateNewRequestToFile(array $data): void {
601        $this->validateNewFile($data);
602        $this->validateSigners($data);
603        $this->signingRequestValidator->validateFileStatus($data);
604    }
605
606    public function validateNewFile(array $data): void {
607        if (empty($data['name'])) {
608            // TRANSLATORS Error shown when creating a signature request without a document file name.
609            throw new \Exception($this->l10n->t('File name is required'));
610        }
611        $this->fileInputValidator->validateNewFile($data);
612    }
613
614    public function validateSigners(array $data): void {
615        if (empty($data['signers'])) {
616            if (($data['status'] ?? FileStatus::ABLE_TO_SIGN->value) === FileStatus::DRAFT->value) {
617                return;
618            }
619            // TRANSLATORS Error shown when creating a signature request without any signers.
620            throw new \Exception($this->l10n->t('Empty signers list'));
621        }
622        if (!is_array($data['signers'])) {
623            // TRANSLATION This message will be displayed when the request to API with the key signers has a value that is not an array
624            // TRANSLATORS Error shown when the signers list in a signature request is not provided as a list.
625            throw new \Exception($this->l10n->t('Signers list needs to be an array'));
626        }
627
628        $this->signerValidator->validateIdentifySigners($data);
629        $normalizedSigners = $this->signerValidator->normalizeRequestSigners($data['signers']);
630
631        foreach ($normalizedSigners as $signer) {
632            $this->identifyMethod->setAllEntityData($signer);
633        }
634    }
635
636    public function unassociateToUser(int $fileId, int $signRequestId, bool $notify = true): void {
637        $this->signerRemovalService->remove($fileId, $signRequestId, $notify);
638    }
639
640    public function deleteRequestSignature(array $data): void {
641        if (!empty($data['uuid'])) {
642            $signatures = $this->signRequestMapper->getByFileUuid($data['uuid']);
643            $fileData = $this->fileMapper->getByUuid($data['uuid']);
644        } elseif (!empty($data['file']['fileId'])) {
645            $fileData = $this->fileMapper->getById($data['file']['fileId']);
646            $signatures = $this->signRequestMapper->getByFileId($fileData->getId());
647        } else {
648            // TRANSLATORS Error shown when a signature-request action is missing both the document identifier and the file object.
649            throw new \Exception($this->l10n->t('Please provide either UUID or File object'));
650        }
651        foreach ($signatures as $signRequest) {
652            $this->identifyMethod->deleteBySignRequestId($signRequest->getId());
653            $this->signRequestMapper->delete($signRequest);
654        }
655        $this->fileMapper->delete($fileData);
656        $this->fileElementService->deleteVisibleElements($fileData->getId());
657    }
658}