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

LibreSign / libresign / 21007057597

14 Jan 2026 07:27PM UTC coverage: 43.847%. First build
21007057597

Pull #6436

github

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

297 of 827 new or added lines in 35 files covered. (35.91%)

6923 of 15789 relevant lines covered (43.85%)

4.87 hits per line

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

46.81
/lib/Service/RequestSignatureService.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\AppInfo\Application;
12
use OCA\Libresign\Db\File as FileEntity;
13
use OCA\Libresign\Db\FileElementMapper;
14
use OCA\Libresign\Db\FileMapper;
15
use OCA\Libresign\Db\IdentifyMethodMapper;
16
use OCA\Libresign\Db\SignRequest as SignRequestEntity;
17
use OCA\Libresign\Db\SignRequestMapper;
18
use OCA\Libresign\Enum\FileStatus;
19
use OCA\Libresign\Enum\SignatureFlow;
20
use OCA\Libresign\Events\SignRequestCanceledEvent;
21
use OCA\Libresign\Exception\LibresignException;
22
use OCA\Libresign\Handler\DocMdpHandler;
23
use OCA\Libresign\Helper\FileUploadHelper;
24
use OCA\Libresign\Helper\ValidateHelper;
25
use OCA\Libresign\Service\Envelope\EnvelopeFileRelocator;
26
use OCA\Libresign\Service\Envelope\EnvelopeService;
27
use OCA\Libresign\Service\File\Pdf\PdfMetadataExtractor;
28
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
29
use OCP\EventDispatcher\IEventDispatcher;
30
use OCP\Files\IMimeTypeDetector;
31
use OCP\Files\Node;
32
use OCP\Http\Client\IClientService;
33
use OCP\IAppConfig;
34
use OCP\IL10N;
35
use OCP\IUser;
36
use OCP\IUserManager;
37
use Psr\Log\LoggerInterface;
38
use Sabre\DAV\UUIDUtil;
39

40
class RequestSignatureService {
41

42
        public function __construct(
43
                protected FileService $fileService,
44
                protected IL10N $l10n,
45
                protected IdentifyMethodService $identifyMethod,
46
                protected SignRequestMapper $signRequestMapper,
47
                protected IUserManager $userManager,
48
                protected FileMapper $fileMapper,
49
                protected IdentifyMethodMapper $identifyMethodMapper,
50
                protected PdfMetadataExtractor $pdfMetadataExtractor,
51
                protected FileElementService $fileElementService,
52
                protected FileElementMapper $fileElementMapper,
53
                protected FolderService $folderService,
54
                protected IMimeTypeDetector $mimeTypeDetector,
55
                protected ValidateHelper $validateHelper,
56
                protected IClientService $client,
57
                protected DocMdpHandler $docMdpHandler,
58
                protected LoggerInterface $logger,
59
                protected SequentialSigningService $sequentialSigningService,
60
                protected IAppConfig $appConfig,
61
                protected IEventDispatcher $eventDispatcher,
62
                protected FileStatusService $fileStatusService,
63
                protected DocMdpConfigService $docMdpConfigService,
64
                protected EnvelopeService $envelopeService,
65
                protected EnvelopeFileRelocator $envelopeFileRelocator,
66
                protected FileUploadHelper $uploadHelper,
67
                protected SignRequestService $signRequestService,
68
        ) {
69
        }
41✔
70

71
        /**
72
         * Save files - creates single file or envelope based on files count
73
         *
74
         * @return array{file: FileEntity, children: list<FileEntity>}
75
         */
76
        public function saveFiles(array $data): array {
77
                if (empty($data['files'])) {
1✔
78
                        throw new LibresignException('Files parameter is required');
×
79
                }
80

81
                if (count($data['files']) === 1) {
1✔
82
                        $fileData = $data['files'][0];
1✔
83

84
                        $saveData = [
1✔
85
                                'name' => $data['name'] ?? $fileData['name'] ?? '',
1✔
86
                                'userManager' => $data['userManager'],
1✔
87
                                'status' => FileStatus::DRAFT->value,
1✔
88
                                'settings' => $data['settings'],
1✔
89
                        ];
1✔
90

91
                        if (isset($fileData['uploadedFile'])) {
1✔
92
                                $saveData['uploadedFile'] = $fileData['uploadedFile'];
×
93
                        } elseif (isset($fileData['fileNode'])) {
1✔
94
                                $saveData['file'] = ['fileNode' => $fileData['fileNode']];
×
95
                        } else {
96
                                $saveData['file'] = $fileData;
1✔
97
                        }
98

99
                        $savedFile = $this->save($saveData);
1✔
100

101
                        return [
1✔
102
                                'file' => $savedFile,
1✔
103
                                'children' => [$savedFile],
1✔
104
                        ];
1✔
105
                }
106

107
                $result = $this->saveEnvelope([
×
108
                        'files' => $data['files'],
×
109
                        'name' => $data['name'],
×
110
                        'userManager' => $data['userManager'],
×
111
                        'settings' => $data['settings'],
×
112
                        'users' => $data['users'] ?? [],
×
113
                        'status' => $data['status'] ?? FileStatus::DRAFT->value,
×
114
                        'visibleElements' => $data['visibleElements'] ?? [],
×
115
                        'signatureFlow' => $data['signatureFlow'] ?? null,
×
116
                ]);
×
117

118
                return [
×
119
                        'file' => $result['envelope'],
×
120
                        'children' => $result['files'],
×
121
                ];
×
122
        }
123

124
        public function save(array $data): FileEntity {
125
                $file = $this->saveFile($data);
14✔
126
                if (!isset($data['status'])) {
14✔
127
                        $data['status'] = $file->getStatus();
12✔
128
                }
129
                $this->sequentialSigningService->setFile($file);
14✔
130
                $this->associateToSigners($data, $file);
14✔
131
                $this->propagateSignersToChildren($file, $data);
14✔
132
                $this->saveVisibleElements($data, $file);
14✔
133

134
                return $file;
14✔
135
        }
136

137
        private function propagateSignersToChildren(FileEntity $envelope, array $data): void {
138
                if ($envelope->getNodeType() !== 'envelope' || empty($data['users'])) {
14✔
139
                        return;
14✔
140
                }
141

142
                $children = $this->fileMapper->getChildrenFiles($envelope->getId());
×
143

144
                $dataWithoutNotification = $data;
×
145
                foreach ($dataWithoutNotification['users'] as &$user) {
×
146
                        $user['notify'] = 0;
×
147
                }
148

149
                foreach ($children as $child) {
×
150
                        $this->identifyMethod->clearCache();
×
151
                        $this->sequentialSigningService->setFile($child);
×
152
                        $this->associateToSigners($dataWithoutNotification, $child);
×
153
                }
154

155
                if ($envelope->getStatus() > FileStatus::DRAFT->value) {
×
156
                        $this->fileStatusService->propagateStatusToChildren($envelope->getId(), $envelope->getStatus());
×
157
                }
158
        }
159

160
        public function saveEnvelope(array $data): array {
161
                $this->envelopeService->validateEnvelopeConstraints(count($data['files']));
×
162

163
                $envelopeName = $data['name'] ?: $this->l10n->t('Envelope %s', [date('Y-m-d H:i:s')]);
×
164
                $userManager = $data['userManager'] ?? null;
×
165
                $userId = $userManager instanceof IUser ? $userManager->getUID() : null;
×
166
                $filesCount = count($data['files']);
×
167

168
                $envelope = null;
×
169
                $files = [];
×
170
                $createdNodes = [];
×
171

172
                try {
173
                        $envelopePath = $data['settings']['path'] ?? null;
×
174
                        $envelope = $this->envelopeService->createEnvelope($envelopeName, $userId, $filesCount, $envelopePath);
×
175

176
                        $envelopeFolder = $this->envelopeService->getEnvelopeFolder($envelope);
×
177
                        $envelopeSettings = array_merge($data['settings'] ?? [], [
×
178
                                'envelopeFolderId' => $envelopeFolder->getId(),
×
179
                        ]);
×
180

181
                        foreach ($data['files'] as $fileData) {
×
182
                                $node = $this->processFileData($fileData, $userManager, $envelopeSettings);
×
183
                                $createdNodes[] = $node;
×
184

185
                                $fileData['node'] = $node;
×
186
                                $fileEntity = $this->createFileForEnvelope($fileData, $userManager, $envelopeSettings);
×
187
                                $this->envelopeService->addFileToEnvelope($envelope->getId(), $fileEntity);
×
188
                                $files[] = $fileEntity;
×
189
                        }
190

191
                        if (!empty($data['users'])) {
×
192
                                $this->sequentialSigningService->setFile($envelope);
×
193
                                $this->associateToSigners($data, $envelope);
×
194
                                $this->propagateSignersToChildren($envelope, $data);
×
195
                        }
196

197
                        return [
×
198
                                'envelope' => $envelope,
×
199
                                'files' => $files,
×
200
                        ];
×
201
                } catch (\Throwable $e) {
×
202
                        $this->rollbackEnvelopeCreation($envelope, $files, $createdNodes);
×
203
                        throw $e;
×
204
                }
205
        }
206

207
        private function processFileData(array $fileData, ?IUser $userManager, array $settings): Node {
NEW
208
                $name = $this->requireFileName($fileData);
×
209

210
                if (isset($fileData['uploadedFile'])) {
×
211
                        $sourceNode = $this->fileService->getNodeFromData([
×
212
                                'userManager' => $userManager,
×
NEW
213
                                'name' => $name,
×
214
                                'uploadedFile' => $fileData['uploadedFile'],
×
215
                                'settings' => $settings,
×
216
                        ]);
×
217
                } else {
218
                        $sourceNode = $this->fileService->getNodeFromData([
×
219
                                'userManager' => $userManager,
×
NEW
220
                                'name' => $name,
×
221
                                'file' => $fileData,
×
222
                                'settings' => $settings,
×
223
                        ]);
×
224
                }
225

226
                if (isset($settings['envelopeFolderId'])) {
×
227
                        return $this->envelopeFileRelocator->ensureFileInEnvelopeFolder(
×
228
                                $sourceNode,
×
229
                                $settings['envelopeFolderId'],
×
230
                                $userManager,
×
231
                        );
×
232
                }
233

234
                return $sourceNode;
×
235
        }
236

237
        private function requireFileName(array $fileData): string {
NEW
238
                $name = trim((string)($fileData['name'] ?? ''));
×
NEW
239
                if ($name === '') {
×
NEW
240
                        throw new LibresignException($this->l10n->t('File name is required'));
×
241
                }
NEW
242
                return $name;
×
243
        }
244

245
        private function rollbackEnvelopeCreation(?FileEntity $envelope, array $files, array $createdNodes): void {
246
                $this->rollbackCreatedNodes($createdNodes);
×
247
                $this->rollbackCreatedFiles($files);
×
248
                $this->rollbackEnvelope($envelope);
×
249
        }
250

251
        private function rollbackCreatedNodes(array $nodes): void {
252
                foreach ($nodes as $node) {
×
253
                        try {
254
                                $node->delete();
×
255
                        } catch (\Throwable $deleteError) {
×
256
                                $this->logger->error('Failed to rollback created node in envelope', [
×
257
                                        'nodeId' => $node->getId(),
×
258
                                        'error' => $deleteError->getMessage(),
×
259
                                ]);
×
260
                        }
261
                }
262
        }
263

264
        private function rollbackCreatedFiles(array $files): void {
265
                foreach ($files as $file) {
×
266
                        try {
267
                                $this->fileMapper->delete($file);
×
268
                        } catch (\Throwable $deleteError) {
×
269
                                $this->logger->error('Failed to rollback created file entity in envelope', [
×
270
                                        'fileId' => $file->getId(),
×
271
                                        'error' => $deleteError->getMessage(),
×
272
                                ]);
×
273
                        }
274
                }
275
        }
276

277
        private function rollbackEnvelope(?FileEntity $envelope): void {
278
                if ($envelope === null) {
×
279
                        return;
×
280
                }
281

282
                try {
283
                        $this->fileMapper->delete($envelope);
×
284
                } catch (\Throwable $deleteError) {
×
285
                        $this->logger->error('Failed to rollback created envelope', [
×
286
                                'envelopeId' => $envelope->getId(),
×
287
                                'error' => $deleteError->getMessage(),
×
288
                        ]);
×
289
                }
290
        }
291

292
        private function createFileForEnvelope(array $fileData, ?IUser $userManager, array $settings): FileEntity {
293
                if (!isset($fileData['node'])) {
×
294
                        throw new \InvalidArgumentException('Node not provided in file data');
×
295
                }
296

297
                $node = $fileData['node'];
×
298
                $fileName = $fileData['name'] ?? $node->getName();
×
299

300
                return $this->saveFile([
×
301
                        'file' => ['fileNode' => $node],
×
302
                        'name' => $fileName,
×
303
                        'userManager' => $userManager,
×
304
                        'status' => FileStatus::DRAFT->value,
×
305
                        'settings' => $settings,
×
306
                ]);
×
307
        }
308

309
        /**
310
         * Save file data
311
         *
312
         * @param array{?userManager: IUser, ?signRequest: SignRequest, name: string, callback: string, uuid?: ?string, status: int, file?: array{fileId?: int, fileNode?: Node}} $data
313
         */
314
        public function saveFile(array $data): FileEntity {
315
                if (!empty($data['uuid'])) {
15✔
316
                        $file = $this->fileMapper->getByUuid($data['uuid']);
1✔
317
                        $this->updateSignatureFlowIfAllowed($file, $data);
1✔
318
                        if (!empty($data['name'])) {
1✔
319
                                $file->setName($data['name']);
×
320
                                $this->fileMapper->update($file);
×
321
                        }
322
                        return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
1✔
323
                }
324
                $fileId = null;
15✔
325
                if (isset($data['file']['fileNode']) && $data['file']['fileNode'] instanceof Node) {
15✔
326
                        $fileId = $data['file']['fileNode']->getId();
×
327
                } elseif (!empty($data['file']['fileId'])) {
15✔
328
                        $fileId = $data['file']['fileId'];
×
329
                }
330
                if (!is_null($fileId)) {
15✔
331
                        try {
332
                                $file = $this->fileMapper->getByNodeId($fileId);
×
333
                                $this->updateSignatureFlowIfAllowed($file, $data);
×
334
                                return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
×
335
                        } catch (\Throwable) {
×
336
                        }
337
                }
338

339
                $node = $this->fileService->getNodeFromData($data);
15✔
340

341
                $file = new FileEntity();
15✔
342
                $file->setNodeId($node->getId());
15✔
343
                if ($data['userManager'] instanceof IUser) {
15✔
344
                        $file->setUserId($data['userManager']->getUID());
15✔
345
                } elseif ($data['signRequest'] instanceof SignRequestEntity) {
×
346
                        $file->setSignRequestId($data['signRequest']->getId());
×
347
                }
348
                $file->setUuid(UUIDUtil::getUUID());
15✔
349
                $file->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
15✔
350
                $metadata = $this->getFileMetadata($node);
15✔
351
                $name = trim((string)($data['name'] ?? ''));
15✔
352
                if ($name === '') {
15✔
353
                        $name = $node->getName();
×
354
                }
355
                $file->setName($this->removeExtensionFromName($name, $metadata));
15✔
356
                $file->setMetadata($metadata);
15✔
357
                if (!empty($data['callback'])) {
15✔
358
                        $file->setCallback($data['callback']);
×
359
                }
360
                if (isset($data['status'])) {
15✔
361
                        $file->setStatus($data['status']);
2✔
362
                } else {
363
                        $file->setStatus(FileStatus::ABLE_TO_SIGN->value);
13✔
364
                }
365

366
                if (isset($data['parentFileId'])) {
15✔
367
                        $file->setParentFileId($data['parentFileId']);
×
368
                }
369

370
                $this->setSignatureFlow($file, $data);
15✔
371
                $this->setDocMdpLevelFromGlobalConfig($file);
15✔
372

373
                $this->fileMapper->insert($file);
15✔
374
                return $file;
15✔
375
        }
376

377
        private function updateSignatureFlowIfAllowed(FileEntity $file, array $data): void {
378
                $adminFlow = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::NONE->value);
1✔
379
                $adminForcedConfig = $adminFlow !== SignatureFlow::NONE->value;
1✔
380

381
                if ($adminForcedConfig) {
1✔
382
                        $adminFlowEnum = SignatureFlow::from($adminFlow);
×
383
                        if ($file->getSignatureFlowEnum() !== $adminFlowEnum) {
×
384
                                $file->setSignatureFlowEnum($adminFlowEnum);
×
385
                                $this->fileMapper->update($file);
×
386
                        }
387
                        return;
×
388
                }
389

390
                if (isset($data['signatureFlow']) && !empty($data['signatureFlow'])) {
1✔
391
                        $newFlow = SignatureFlow::from($data['signatureFlow']);
×
392
                        if ($file->getSignatureFlowEnum() !== $newFlow) {
×
393
                                $file->setSignatureFlowEnum($newFlow);
×
394
                                $this->fileMapper->update($file);
×
395
                        }
396
                }
397
        }
398

399
        private function setSignatureFlow(FileEntity $file, array $data): void {
400
                $adminFlow = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::NONE->value);
15✔
401

402
                if (isset($data['signatureFlow']) && !empty($data['signatureFlow'])) {
15✔
403
                        $file->setSignatureFlowEnum(SignatureFlow::from($data['signatureFlow']));
×
404
                } elseif ($adminFlow !== SignatureFlow::NONE->value) {
15✔
405
                        $file->setSignatureFlowEnum(SignatureFlow::from($adminFlow));
×
406
                } else {
407
                        $file->setSignatureFlowEnum(SignatureFlow::NONE);
15✔
408
                }
409
        }
410

411
        private function setDocMdpLevelFromGlobalConfig(FileEntity $file): void {
412
                if ($this->docMdpConfigService->isEnabled()) {
15✔
413
                        $docmdpLevel = $this->docMdpConfigService->getLevel();
×
414
                        $file->setDocmdpLevelEnum($docmdpLevel);
×
415
                }
416
        }
417

418
        private function getFileMetadata(\OCP\Files\Node $node): array {
419
                $metadata = [];
18✔
420
                if ($extension = strtolower($node->getExtension())) {
18✔
421
                        $metadata = [
17✔
422
                                'extension' => $extension,
17✔
423
                        ];
17✔
424
                        if ($metadata['extension'] === 'pdf') {
17✔
425
                                $this->pdfMetadataExtractor->setFile($node);
16✔
426
                                $metadata = array_merge(
16✔
427
                                        $metadata,
16✔
428
                                        $this->pdfMetadataExtractor->getPageDimensions()
16✔
429
                                );
16✔
430
                                $metadata['pdfVersion'] = $this->pdfMetadataExtractor->getPdfVersion();
16✔
431
                        }
432
                }
433
                return $metadata;
18✔
434
        }
435

436
        private function removeExtensionFromName(string $name, array $metadata): string {
437
                if (!isset($metadata['extension'])) {
15✔
438
                        return $name;
×
439
                }
440
                $extensionPattern = '/\.' . preg_quote($metadata['extension'], '/') . '$/i';
15✔
441
                $result = preg_replace($extensionPattern, '', $name);
15✔
442
                return $result ?? $name;
15✔
443
        }
444

445
        private function deleteIdentifyMethodIfNotExits(array $users, FileEntity $file): void {
446
                $signRequests = $this->signRequestMapper->getByFileId($file->getId());
13✔
447
                foreach ($signRequests as $key => $signRequest) {
13✔
448
                        $identifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequest->getId());
1✔
449
                        if (empty($identifyMethods)) {
1✔
450
                                $this->unassociateToUser($file->getId(), $signRequest->getId());
×
451
                                continue;
×
452
                        }
453
                        foreach ($identifyMethods as $methodName => $list) {
1✔
454
                                foreach ($list as $method) {
1✔
455
                                        $exists[$key]['identify'][$methodName] = $method->getEntity()->getIdentifierValue();
1✔
456
                                        if (!$this->identifyMethodExists($users, $method)) {
1✔
457
                                                $this->unassociateToUser($file->getId(), $signRequest->getId());
1✔
458
                                                continue 3;
1✔
459
                                        }
460
                                }
461
                        }
462
                }
463
        }
464

465
        private function identifyMethodExists(array $users, IIdentifyMethod $identifyMethod): bool {
466
                foreach ($users as $user) {
1✔
467
                        if (!empty($user['identifyMethods'])) {
1✔
468
                                foreach ($user['identifyMethods'] as $data) {
×
469
                                        if ($identifyMethod->getEntity()->getIdentifierKey() !== $data['method']) {
×
470
                                                continue;
×
471
                                        }
472
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $data['value']) {
×
473
                                                return true;
×
474
                                        }
475
                                }
476
                        } else {
477
                                foreach ($user['identify'] as $method => $value) {
1✔
478
                                        if ($identifyMethod->getEntity()->getIdentifierKey() !== $method) {
1✔
479
                                                continue;
×
480
                                        }
481
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $value) {
1✔
482
                                                return true;
×
483
                                        }
484
                                }
485
                        }
486
                }
487
                return false;
1✔
488
        }
489

490
        /**
491
         * @return SignRequestEntity[]
492
         *
493
         * @psalm-return list<SignRequestEntity>
494
         */
495
        private function associateToSigners(array $data, FileEntity $file): array {
496
                $return = [];
14✔
497
                if (!empty($data['users'])) {
14✔
498
                        $this->deleteIdentifyMethodIfNotExits($data['users'], $file);
13✔
499

500
                        $this->sequentialSigningService->resetOrderCounter();
13✔
501
                        $fileStatus = $data['status'] ?? null;
13✔
502

503
                        foreach ($data['users'] as $user) {
13✔
504
                                $userProvidedOrder = isset($user['signingOrder']) ? (int)$user['signingOrder'] : null;
13✔
505
                                $signingOrder = $this->sequentialSigningService->determineSigningOrder($userProvidedOrder);
13✔
506
                                $signerStatus = $user['status'] ?? null;
13✔
507
                                $shouldNotify = !isset($user['notify']) || $user['notify'] !== 0;
13✔
508

509
                                if (isset($user['identifyMethods'])) {
13✔
510
                                        foreach ($user['identifyMethods'] as $identifyMethod) {
×
511
                                                $return[] = $this->signRequestService->createOrUpdateSignRequest(
×
512
                                                        identifyMethods: [
×
513
                                                                $identifyMethod['method'] => $identifyMethod['value'],
×
514
                                                        ],
×
515
                                                        displayName: $user['displayName'] ?? '',
×
516
                                                        description: $user['description'] ?? '',
×
517
                                                        notify: $shouldNotify,
×
518
                                                        fileId: $file->getId(),
×
519
                                                        signingOrder: $signingOrder,
×
520
                                                        fileStatus: $fileStatus,
×
521
                                                        signerStatus: $signerStatus,
×
522
                                                );
×
523
                                        }
524
                                } else {
525
                                        $return[] = $this->signRequestService->createOrUpdateSignRequest(
13✔
526
                                                identifyMethods: $user['identify'],
13✔
527
                                                displayName: $user['displayName'] ?? '',
13✔
528
                                                description: $user['description'] ?? '',
13✔
529
                                                notify: $shouldNotify,
13✔
530
                                                fileId: $file->getId(),
13✔
531
                                                signingOrder: $signingOrder,
13✔
532
                                                fileStatus: $fileStatus,
13✔
533
                                                signerStatus: $signerStatus,
13✔
534
                                        );
13✔
535
                                }
536
                        }
537
                }
538
                return $return;
14✔
539
        }
540

541

542

543
        private function saveVisibleElements(array $data, FileEntity $file): array {
544
                if (empty($data['visibleElements'])) {
17✔
545
                        return [];
15✔
546
                }
547
                $persisted = [];
2✔
548
                foreach ($data['visibleElements'] as $element) {
2✔
549
                        if ($file->isEnvelope() && !empty($element['signRequestId'])) {
2✔
550
                                $envelopeSignRequest = $this->signRequestMapper->getById((int)$element['signRequestId']);
×
551
                                // Only translate if the provided SR belongs to the envelope itself
552
                                if ($envelopeSignRequest && $envelopeSignRequest->getFileId() === $file->getId()) {
×
553
                                        $childrenSrs = $this->signRequestMapper->getByEnvelopeChildrenAndIdentifyMethod($file->getId(), (int)$element['signRequestId']);
×
554
                                        foreach ($childrenSrs as $childSr) {
×
555
                                                if ($childSr->getFileId() === (int)$element['fileId']) {
×
556
                                                        $element['signRequestId'] = $childSr->getId();
×
557
                                                        break;
×
558
                                                }
559
                                        }
560
                                }
561
                        }
562

563
                        $persisted[] = $this->fileElementService->saveVisibleElement($element);
2✔
564
                }
565
                return $persisted;
2✔
566
        }
567

568

569
        public function validateNewRequestToFile(array $data): void {
570
                $this->validateNewFile($data);
7✔
571
                $this->validateUsers($data);
6✔
572
                $this->validateHelper->validateFileStatus($data);
2✔
573
        }
574

575
        public function validateNewFile(array $data): void {
576
                if (empty($data['name'])) {
7✔
577
                        throw new \Exception($this->l10n->t('File name is required'));
1✔
578
                }
579
                $this->validateHelper->validateNewFile($data);
6✔
580
        }
581

582
        public function validateUsers(array $data): void {
583
                if (empty($data['users'])) {
6✔
584
                        if (($data['status'] ?? FileStatus::ABLE_TO_SIGN->value) === FileStatus::DRAFT->value) {
3✔
585
                                return;
×
586
                        }
587
                        throw new \Exception($this->l10n->t('Empty users list'));
3✔
588
                }
589
                if (!is_array($data['users'])) {
3✔
590
                        // TRANSLATION This message will be displayed when the request to API with the key users has a value that is not an array
591
                        throw new \Exception($this->l10n->t('User list needs to be an array'));
1✔
592
                }
593
                foreach ($data['users'] as $user) {
2✔
594
                        if (!array_key_exists('identify', $user)) {
2✔
595
                                throw new \Exception('Identify key not found');
×
596
                        }
597
                        $this->identifyMethod->setAllEntityData($user);
2✔
598
                }
599
        }
600

601

602

603
        public function unassociateToUser(int $fileId, int $signRequestId): void {
604
                $file = $this->fileMapper->getById($fileId);
2✔
605
                $signRequest = $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
2✔
606
                $deletedOrder = $signRequest->getSigningOrder();
2✔
607
                $groupedIdentifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequestId);
2✔
608

609
                $this->dispatchCancellationEventIfNeeded($signRequest, $file, $groupedIdentifyMethods);
2✔
610

611
                try {
612
                        $this->signRequestMapper->delete($signRequest);
2✔
613
                        $this->identifyMethod->deleteBySignRequestId($signRequestId);
2✔
614
                        $visibleElements = $this->fileElementMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
2✔
615
                        foreach ($visibleElements as $visibleElement) {
2✔
616
                                $this->fileElementMapper->delete($visibleElement);
×
617
                        }
618

619
                        $this->sequentialSigningService
2✔
620
                                ->setFile($file)
2✔
621
                                ->reorderAfterDeletion($file->getId(), $deletedOrder);
2✔
622

623
                        $this->propagateSignerDeletionToChildren($file, $signRequest);
2✔
624
                } catch (\Throwable) {
×
625
                }
626
        }
627

628
        private function propagateSignerDeletionToChildren(FileEntity $envelope, SignRequestEntity $deletedSignRequest): void {
629
                if ($envelope->getNodeType() !== 'envelope') {
2✔
630
                        return;
2✔
631
                }
632

633
                $children = $this->fileMapper->getChildrenFiles($envelope->getId());
×
634

635
                $identifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($deletedSignRequest->getId());
×
636
                if (empty($identifyMethods)) {
×
637
                        return;
×
638
                }
639

640
                foreach ($children as $child) {
×
641
                        try {
642
                                $this->identifyMethod->clearCache();
×
643
                                $childSignRequest = $this->signRequestService->getSignRequestByIdentifyMethod(
×
644
                                        current(reset($identifyMethods)),
×
645
                                        $child->getId()
×
646
                                );
×
647

648
                                if ($childSignRequest->getId()) {
×
649
                                        $this->unassociateToUser($child->getId(), $childSignRequest->getId());
×
650
                                }
651
                        } catch (\Throwable $e) {
×
652
                                continue;
×
653
                        }
654
                }
655
        }
656

657
        private function dispatchCancellationEventIfNeeded(
658
                SignRequestEntity $signRequest,
659
                FileEntity $file,
660
                array $groupedIdentifyMethods,
661
        ): void {
662
                if ($signRequest->getStatus() !== \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN->value) {
2✔
663
                        return;
×
664
                }
665

666
                try {
667
                        foreach ($groupedIdentifyMethods as $identifyMethods) {
2✔
668
                                foreach ($identifyMethods as $identifyMethod) {
2✔
669
                                        $event = new SignRequestCanceledEvent(
2✔
670
                                                $signRequest,
2✔
671
                                                $file,
2✔
672
                                                $identifyMethod,
2✔
673
                                        );
2✔
674
                                        $this->eventDispatcher->dispatchTyped($event);
2✔
675
                                }
676
                        }
677
                } catch (\Throwable $e) {
×
678
                        $this->logger->error('Error dispatching SignRequestCanceledEvent: ' . $e->getMessage(), ['exception' => $e]);
×
679
                }
680
        }
681

682
        public function deleteRequestSignature(array $data): void {
683
                if (!empty($data['uuid'])) {
1✔
684
                        $signatures = $this->signRequestMapper->getByFileUuid($data['uuid']);
×
685
                        $fileData = $this->fileMapper->getByUuid($data['uuid']);
×
686
                } elseif (!empty($data['file']['fileId'])) {
1✔
687
                        $fileData = $this->fileMapper->getById($data['file']['fileId']);
1✔
688
                        $signatures = $this->signRequestMapper->getByFileId($fileData->getId());
1✔
689
                } else {
690
                        throw new \Exception($this->l10n->t('Please provide either UUID or File object'));
×
691
                }
692
                foreach ($signatures as $signRequest) {
1✔
693
                        $this->identifyMethod->deleteBySignRequestId($signRequest->getId());
1✔
694
                        $this->signRequestMapper->delete($signRequest);
1✔
695
                }
696
                $this->fileMapper->delete($fileData);
1✔
697
                $this->fileElementService->deleteVisibleElements($fileData->getId());
1✔
698
        }
699
}
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