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

LibreSign / libresign / 20882165631

10 Jan 2026 05:56PM UTC coverage: 44.646%. First build
20882165631

Pull #6433

github

web-flow
Merge eead2e4b3 into ecd36974e
Pull Request #6433: refactor: move all constants to FileStatus enum

31 of 56 new or added lines in 16 files covered. (55.36%)

6742 of 15101 relevant lines covered (44.65%)

5.01 hits per line

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

50.75
/lib/Service/SignFileService.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 DateTime;
12
use DateTimeInterface;
13
use Exception;
14
use InvalidArgumentException;
15
use OC\AppFramework\Http as AppFrameworkHttp;
16
use OC\User\NoUserException;
17
use OCA\Libresign\AppInfo\Application;
18
use OCA\Libresign\DataObjects\VisibleElementAssoc;
19
use OCA\Libresign\Db\File as FileEntity;
20
use OCA\Libresign\Db\FileElement;
21
use OCA\Libresign\Db\FileElementMapper;
22
use OCA\Libresign\Db\FileMapper;
23
use OCA\Libresign\Db\IdDocs;
24
use OCA\Libresign\Db\IdDocsMapper;
25
use OCA\Libresign\Db\IdentifyMethod;
26
use OCA\Libresign\Db\IdentifyMethodMapper;
27
use OCA\Libresign\Db\SignRequest as SignRequestEntity;
28
use OCA\Libresign\Db\SignRequestMapper;
29
use OCA\Libresign\Db\UserElementMapper;
30
use OCA\Libresign\Enum\FileStatus;
31
use OCA\Libresign\Events\SignedEventFactory;
32
use OCA\Libresign\Exception\LibresignException;
33
use OCA\Libresign\Handler\DocMdpHandler;
34
use OCA\Libresign\Handler\FooterHandler;
35
use OCA\Libresign\Handler\PdfTk\Pdf;
36
use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
37
use OCA\Libresign\Handler\SignEngine\SignEngineFactory;
38
use OCA\Libresign\Handler\SignEngine\SignEngineHandler;
39
use OCA\Libresign\Helper\JSActions;
40
use OCA\Libresign\Helper\ValidateHelper;
41
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
42
use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\IToken;
43
use OCP\AppFramework\Db\DoesNotExistException;
44
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
45
use OCP\AppFramework\Utility\ITimeFactory;
46
use OCP\EventDispatcher\IEventDispatcher;
47
use OCP\Files\File;
48
use OCP\Files\IRootFolder;
49
use OCP\Files\NotPermittedException;
50
use OCP\Http\Client\IClientService;
51
use OCP\IAppConfig;
52
use OCP\IDateTimeZone;
53
use OCP\IL10N;
54
use OCP\ITempManager;
55
use OCP\IURLGenerator;
56
use OCP\IUser;
57
use OCP\IUserManager;
58
use OCP\IUserSession;
59
use OCP\Security\Events\GenerateSecurePasswordEvent;
60
use OCP\Security\ISecureRandom;
61
use Psr\Log\LoggerInterface;
62
use RuntimeException;
63
use Sabre\DAV\UUIDUtil;
64

65
class SignFileService {
66
        private ?SignRequestEntity $signRequest = null;
67
        private string $password = '';
68
        private ?FileEntity $libreSignFile = null;
69
        /** @var VisibleElementAssoc[] */
70
        private $elements = [];
71
        private bool $signWithoutPassword = false;
72
        private ?File $fileToSign = null;
73
        private ?File $createdSignedFile = null;
74
        private string $userUniqueIdentifier = '';
75
        private string $friendlyName = '';
76
        private ?IUser $user = null;
77
        private ?SignEngineHandler $engine = null;
78

79
        public function __construct(
80
                protected IL10N $l10n,
81
                private FileMapper $fileMapper,
82
                private SignRequestMapper $signRequestMapper,
83
                private IdDocsMapper $idDocsMapper,
84
                private FooterHandler $footerHandler,
85
                protected FolderService $folderService,
86
                private IClientService $client,
87
                private IUserManager $userManager,
88
                protected LoggerInterface $logger,
89
                private IAppConfig $appConfig,
90
                protected ValidateHelper $validateHelper,
91
                private SignerElementsService $signerElementsService,
92
                private IRootFolder $root,
93
                private IUserSession $userSession,
94
                private IDateTimeZone $dateTimeZone,
95
                private FileElementMapper $fileElementMapper,
96
                private UserElementMapper $userElementMapper,
97
                private IEventDispatcher $eventDispatcher,
98
                protected ISecureRandom $secureRandom,
99
                private IURLGenerator $urlGenerator,
100
                private IdentifyMethodMapper $identifyMethodMapper,
101
                private ITempManager $tempManager,
102
                private IdentifyMethodService $identifyMethodService,
103
                private ITimeFactory $timeFactory,
104
                protected SignEngineFactory $signEngineFactory,
105
                private SignedEventFactory $signedEventFactory,
106
                private Pdf $pdf,
107
                private DocMdpHandler $docMdpHandler,
108
                private PdfSignatureDetectionService $pdfSignatureDetectionService,
109
                private SequentialSigningService $sequentialSigningService,
110
                private FileStatusService $fileStatusService,
111
        ) {
112
        }
139✔
113

114
        /**
115
         * Can delete sing request
116
         */
117
        public function canDeleteRequestSignature(array $data): void {
118
                if (!empty($data['uuid'])) {
2✔
119
                        $signatures = $this->signRequestMapper->getByFileUuid($data['uuid']);
2✔
120
                } elseif (!empty($data['file']['fileId'])) {
×
121
                        $signatures = $this->signRequestMapper->getByNodeId($data['file']['fileId']);
×
122
                } else {
123
                        throw new \Exception($this->l10n->t('Please provide either UUID or File object'));
×
124
                }
125
                $signed = array_filter($signatures, fn ($s) => $s->getSigned());
2✔
126
                if ($signed) {
2✔
127
                        throw new \Exception($this->l10n->t('Document already signed'));
1✔
128
                }
129
                array_walk($data['users'], function ($user) use ($signatures): void {
1✔
130
                        $exists = array_filter($signatures, function (SignRequestEntity $signRequest) use ($user) {
1✔
131
                                $identifyMethod = $this->identifyMethodService->getIdentifiedMethod($signRequest->getId());
1✔
132
                                if ($identifyMethod->getName() === 'email') {
1✔
133
                                        return $identifyMethod->getEntity()->getIdentifierValue() === $user['email'];
×
134
                                }
135
                                return false;
1✔
136
                        });
1✔
137
                        if (!$exists) {
1✔
138
                                throw new \Exception($this->l10n->t('No signature was requested to %s', $user['email']));
1✔
139
                        }
140
                });
1✔
141
        }
142

143
        public function notifyCallback(File $file): void {
144
                $uri = $this->libreSignFile->getCallback();
1✔
145
                if (!$uri) {
1✔
146
                        $uri = $this->appConfig->getValueString(Application::APP_ID, 'webhook_sign_url');
×
147
                        if (!$uri) {
×
148
                                return;
×
149
                        }
150
                }
151
                $options = [
1✔
152
                        'multipart' => [
1✔
153
                                [
1✔
154
                                        'name' => 'uuid',
1✔
155
                                        'contents' => $this->libreSignFile->getUuid(),
1✔
156
                                ],
1✔
157
                                [
1✔
158
                                        'name' => 'status',
1✔
159
                                        'contents' => $this->libreSignFile->getStatus(),
1✔
160
                                ],
1✔
161
                                [
1✔
162
                                        'name' => 'file',
1✔
163
                                        'contents' => $file->fopen('r'),
1✔
164
                                        'filename' => $file->getName()
1✔
165
                                ]
1✔
166
                        ]
1✔
167
                ];
1✔
168
                $this->client->newClient()->post($uri, $options);
1✔
169
        }
170

171
        /**
172
         * @return static
173
         */
174
        public function setLibreSignFile(FileEntity $libreSignFile): self {
175
                $this->libreSignFile = $libreSignFile;
39✔
176
                return $this;
39✔
177
        }
178

179
        public function setUserUniqueIdentifier(string $identifier): self {
180
                $this->userUniqueIdentifier = $identifier;
×
181
                return $this;
×
182
        }
183

184
        public function setFriendlyName(string $friendlyName): self {
185
                $this->friendlyName = $friendlyName;
×
186
                return $this;
×
187
        }
188

189
        /**
190
         * @return static
191
         */
192
        public function setSignRequest(SignRequestEntity $signRequest): self {
193
                $this->signRequest = $signRequest;
66✔
194
                return $this;
66✔
195
        }
196

197
        /**
198
         * @return static
199
         */
200
        public function setSignWithoutPassword(bool $signWithoutPassword = true): self {
201
                $this->signWithoutPassword = $signWithoutPassword;
2✔
202
                return $this;
2✔
203
        }
204

205
        /**
206
         * @return static
207
         */
208
        public function setPassword(?string $password = null): self {
209
                $this->password = $password;
2✔
210
                return $this;
2✔
211
        }
212

213
        public function setCurrentUser(?IUser $user): self {
214
                $this->user = $user;
24✔
215
                return $this;
24✔
216
        }
217

218
        public function setVisibleElements(array $list): self {
219
                if (!$this->signRequest instanceof SignRequestEntity) {
14✔
220
                        return $this;
×
221
                }
222
                $fileElements = $this->fileElementMapper->getByFileIdAndSignRequestId($this->signRequest->getFileId(), $this->signRequest->getId());
14✔
223
                $canCreateSignature = $this->signerElementsService->canCreateSignature();
14✔
224

225
                foreach ($fileElements as $fileElement) {
14✔
226
                        $this->elements[] = $this->buildVisibleElementAssoc($fileElement, $list, $canCreateSignature);
12✔
227
                }
228

229
                return $this;
5✔
230
        }
231

232
        private function buildVisibleElementAssoc(FileElement $fileElement, array $list, bool $canCreateSignature): VisibleElementAssoc {
233
                if (!$canCreateSignature) {
12✔
234
                        return new VisibleElementAssoc($fileElement);
1✔
235
                }
236

237
                $element = $this->array_find($list, fn (array $element): bool => ($element['documentElementId'] ?? '') === $fileElement->getId());
11✔
238
                $nodeId = $this->getNodeId($element, $fileElement);
11✔
239

240
                return $this->bindFileElementWithTempFile($fileElement, $nodeId);
7✔
241
        }
242

243
        private function getNodeId(?array $element, FileElement $fileElement): int {
244
                if ($this->isValidElement($element)) {
11✔
245
                        return (int)$element['profileNodeId'];
7✔
246
                }
247

248
                return $this->retrieveUserElement($fileElement);
×
249
        }
250

251
        private function isValidElement(?array $element): bool {
252
                if (is_array($element) && !empty($element['profileNodeId']) && is_int($element['profileNodeId'])) {
11✔
253
                        return true;
7✔
254
                }
255
                $this->logger->error('Invalid data provided for signing file.', ['element' => $element]);
4✔
256
                throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
4✔
257
        }
258

259
        private function retrieveUserElement(FileElement $fileElement): int {
260
                try {
261
                        if (!$this->user instanceof IUser) {
×
262
                                throw new Exception('User not set');
×
263
                        }
264
                        $userElement = $this->userElementMapper->findOne([
×
265
                                'user_id' => $this->user->getUID(),
×
266
                                'type' => $fileElement->getType(),
×
267
                        ]);
×
268
                } catch (MultipleObjectsReturnedException|DoesNotExistException|Exception) {
×
269
                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
×
270
                }
271
                return $userElement->getNodeId();
×
272
        }
273

274
        private function bindFileElementWithTempFile(FileElement $fileElement, int $nodeId): VisibleElementAssoc {
275
                try {
276
                        $node = $this->getNode($nodeId);
7✔
277
                        if (!$node) {
4✔
278
                                throw new \Exception('Node content is empty or unavailable.');
4✔
279
                        }
280
                } catch (\Throwable) {
4✔
281
                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
4✔
282
                }
283

284
                $tempFile = $this->tempManager->getTemporaryFile('_' . $nodeId . '.png');
3✔
285
                $content = $node->getContent();
3✔
286
                if (empty($content)) {
3✔
287
                        $this->logger->error('Failed to retrieve content for node.', ['nodeId' => $nodeId, 'fileElement' => $fileElement]);
1✔
288
                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
1✔
289
                }
290
                file_put_contents($tempFile, $content);
2✔
291
                return new VisibleElementAssoc($fileElement, $tempFile);
2✔
292
        }
293

294
        private function getNode(int $nodeId): ?File {
295
                if ($this->user instanceof IUser) {
7✔
296
                        return $this->folderService->getFileByNodeId($nodeId);
6✔
297
                }
298

299
                $filesOfElementes = $this->signerElementsService->getElementsFromSession();
1✔
300
                return $this->array_find($filesOfElementes, fn ($file) => $file->getId() === $nodeId);
1✔
301
        }
302

303
        /**
304
         * Fallback to PHP < 8.4
305
         *
306
         * Reference: https://www.php.net/manual/en/function.array-find.php#130257
307
         *
308
         * @todo remove this after minor PHP version is >= 8.4
309
         * @deprecated This method will be removed once the minimum PHP version is >= 8.4. Use native array_find instead.
310
         */
311
        private function array_find(array $array, callable $callback): mixed {
312
                foreach ($array as $key => $value) {
12✔
313
                        if ($callback($value, $key)) {
11✔
314
                                return $value;
10✔
315
                        }
316
                }
317

318
                return null;
3✔
319
        }
320

321
        /**
322
         * @return VisibleElementAssoc[]
323
         */
324
        public function getVisibleElements(): array {
325
                return $this->elements;
7✔
326
        }
327

328
        public function sign(): void {
329
                $signRequests = $this->getSignRequestsToSign();
18✔
330

331
                if (empty($signRequests)) {
18✔
332
                        throw new LibresignException('No sign requests found to process');
×
333
                }
334

335
                $envelopeLastSignedDate = null;
18✔
336

337
                foreach ($signRequests as $signRequestData) {
18✔
338
                        $this->libreSignFile = $signRequestData['file'];
18✔
339
                        if ($this->libreSignFile->getSignedHash()) {
18✔
340
                                continue;
×
341
                        }
342
                        $this->signRequest = $signRequestData['signRequest'];
18✔
343
                        $this->engine = null;
18✔
344
                        $this->elements = [];
18✔
345
                        $this->fileToSign = null;
18✔
346

347
                        $this->validateDocMdpAllowsSignatures();
18✔
348

349
                        try {
350
                                $signedFile = $this->getEngine()->sign();
16✔
351
                        } catch (LibresignException|Exception $e) {
×
352
                                $this->cleanupUnsignedSignedFile();
×
353
                                $this->recordSignatureAttempt($e);
×
354

355
                                $isEnvelope = $this->libreSignFile->isEnvelope() || $this->libreSignFile->hasParent();
×
356
                                if (!$isEnvelope) {
×
357
                                        throw $e;
×
358
                                }
359
                                continue;
×
360
                        }
361

362
                        $hash = $this->computeHash($signedFile);
16✔
363
                        $envelopeLastSignedDate = $this->getEngine()->getLastSignedDate();
16✔
364

365
                        $this->updateSignRequest($hash);
16✔
366
                        $this->updateLibreSignFile($signedFile->getId(), $hash);
16✔
367

368
                        $this->dispatchSignedEvent();
16✔
369
                }
370

371
                $envelopeContext = $this->getEnvelopeContext();
16✔
372
                if ($envelopeContext['envelope'] instanceof FileEntity) {
16✔
373
                        $this->updateEnvelopeStatus(
×
374
                                $envelopeContext['envelope'],
×
375
                                $envelopeContext['envelopeSignRequest'] ?? null,
×
376
                                $envelopeLastSignedDate
×
377
                        );
×
378
                }
379
        }
380

381
        /**
382
         * Get sign requests to process.
383
         *
384
         * @return array Array of sign request data with 'file' => FileEntity, 'signRequest' => SignRequestEntity
385
         */
386
        private function getSignRequestsToSign(): array {
387
                if (!$this->libreSignFile->isEnvelope()
19✔
388
                        && !$this->libreSignFile->hasParent()
19✔
389
                ) {
390
                        return [[
18✔
391
                                'file' => $this->libreSignFile,
18✔
392
                                'signRequest' => $this->signRequest,
18✔
393
                        ]];
18✔
394
                }
395

396
                return $this->buildEnvelopeSignRequests();
1✔
397
        }
398

399
        /**
400
         * @return array Array of sign request data with 'file' => FileEntity, 'signRequest' => SignRequestEntity
401
         */
402
        private function buildEnvelopeSignRequests(): array {
403
                $envelopeId = $this->libreSignFile->isEnvelope()
1✔
404
                        ? $this->libreSignFile->getId()
×
405
                        : $this->libreSignFile->getParentFileId();
1✔
406

407
                $childFiles = $this->fileMapper->getChildrenFiles($envelopeId);
1✔
408
                if (empty($childFiles)) {
1✔
409
                        throw new LibresignException('No files found in envelope');
×
410
                }
411

412
                $childSignRequests = $this->signRequestMapper->getByEnvelopeChildrenAndIdentifyMethod(
1✔
413
                        $envelopeId,
1✔
414
                        $this->signRequest->getId()
1✔
415
                );
1✔
416

417
                if (empty($childSignRequests)) {
1✔
418
                        throw new LibresignException('No sign requests found for envelope files');
×
419
                }
420

421
                $signRequestsData = [];
1✔
422
                foreach ($childSignRequests as $childSignRequest) {
1✔
423
                        $childFile = $this->array_find(
1✔
424
                                $childFiles,
1✔
425
                                fn (FileEntity $file) => $file->getId() === $childSignRequest->getFileId()
1✔
426
                        );
1✔
427

428
                        if ($childFile) {
1✔
429
                                $signRequestsData[] = [
1✔
430
                                        'file' => $childFile,
1✔
431
                                        'signRequest' => $childSignRequest,
1✔
432
                                ];
1✔
433
                        }
434
                }
435

436
                return $signRequestsData;
1✔
437
        }
438

439
        /**
440
         * Get envelope context if the current file is or belongs to an envelope.
441
         *
442
         * @return array Array with 'envelope' => FileEntity or null, 'envelopeSignRequest' => SignRequestEntity or null
443
         */
444
        private function getEnvelopeContext(): array {
445
                $result = [
16✔
446
                        'envelope' => null,
16✔
447
                        'envelopeSignRequest' => null,
16✔
448
                ];
16✔
449

450
                if (!$this->libreSignFile->isEnvelope() && !$this->libreSignFile->hasParent()) {
16✔
451
                        return $result;
16✔
452
                }
453

454
                if ($this->libreSignFile->isEnvelope()) {
×
455
                        $result['envelope'] = $this->libreSignFile;
×
456
                        $result['envelopeSignRequest'] = $this->signRequest;
×
457
                        return $result;
×
458
                }
459

460
                try {
461
                        $envelopeId = $this->libreSignFile->isEnvelope()
×
462
                                ? $this->libreSignFile->getId()
×
463
                                : $this->libreSignFile->getParentFileId();
×
464
                        $result['envelope'] = $this->fileMapper->getById($envelopeId);
×
465
                        $identifyMethod = $this->identifyMethodService->getIdentifiedMethod($this->signRequest->getId());
×
466
                        $result['envelopeSignRequest'] = $this->signRequestMapper->getByIdentifyMethodAndFileId(
×
467
                                $identifyMethod,
×
468
                                $result['envelope']->getId()
×
469
                        );
×
470
                } catch (DoesNotExistException $e) {
×
471
                        // Envelope not found or sign request not found, leave as null
472
                }
473

474
                return $result;
×
475
        }
476

477
        private function updateEnvelopeStatus(FileEntity $envelope, ?SignRequestEntity $envelopeSignRequest = null, ?DateTimeInterface $signedDate = null): void {
478
                $childFiles = $this->fileMapper->getChildrenFiles($envelope->getId());
×
479

480
                $totalSignRequests = 0;
×
481
                $signedSignRequests = 0;
×
482

483
                foreach ($childFiles as $childFile) {
×
484
                        $signRequests = $this->signRequestMapper->getByFileId($childFile->getId());
×
485
                        $totalSignRequests += count($signRequests);
×
486

487
                        foreach ($signRequests as $signRequest) {
×
488
                                if ($signRequest->getSigned()) {
×
489
                                        $signedSignRequests++;
×
490
                                }
491
                        }
492
                }
493

494
                if ($totalSignRequests === 0) {
×
NEW
495
                        $envelope->setStatus(FileStatus::DRAFT->value);
×
496
                } elseif ($signedSignRequests === 0) {
×
NEW
497
                        $envelope->setStatus(FileStatus::ABLE_TO_SIGN->value);
×
498
                } elseif ($signedSignRequests === $totalSignRequests) {
×
NEW
499
                        $envelope->setStatus(FileStatus::SIGNED->value);
×
500
                        if ($envelopeSignRequest instanceof SignRequestEntity) {
×
501
                                $envelopeSignRequest->setSigned($signedDate ?: new DateTime());
×
502
                                $envelopeSignRequest->setStatusEnum(\OCA\Libresign\Enum\SignRequestStatus::SIGNED);
×
503
                                $this->signRequestMapper->update($envelopeSignRequest);
×
504
                                $this->sequentialSigningService
×
505
                                        ->setFile($envelope)
×
506
                                        ->releaseNextOrder(
×
507
                                                $envelopeSignRequest->getFileId(),
×
508
                                                $envelopeSignRequest->getSigningOrder()
×
509
                                        );
×
510
                        }
511
                } else {
NEW
512
                        $envelope->setStatusEnum(FileStatus::PARTIAL_SIGNED);
×
513
                }
514

515
                $this->fileMapper->update($envelope);
×
516
        }
517

518
        /**
519
         * @throws LibresignException If the document has DocMDP level 1 (no changes allowed)
520
         */
521
        protected function validateDocMdpAllowsSignatures(): void {
522
                $docmdpLevel = $this->libreSignFile->getDocmdpLevelEnum();
18✔
523

524
                if ($docmdpLevel === \OCA\Libresign\Enum\DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED) {
18✔
525
                        throw new LibresignException(
×
526
                                $this->l10n->t('This document has been certified with no changes allowed. You cannot add more signers to this document.'),
×
527
                                AppFrameworkHttp::STATUS_UNPROCESSABLE_ENTITY
×
528
                        );
×
529
                }
530

531
                if ($docmdpLevel === \OCA\Libresign\Enum\DocMdpLevel::NOT_CERTIFIED) {
18✔
532
                        $resource = $this->getLibreSignFileAsResource();
18✔
533

534
                        try {
535
                                if (!$this->docMdpHandler->allowsAdditionalSignatures($resource)) {
17✔
536
                                        throw new LibresignException(
3✔
537
                                                $this->l10n->t('This document has been certified with no changes allowed. You cannot add more signers to this document.'),
3✔
538
                                                AppFrameworkHttp::STATUS_UNPROCESSABLE_ENTITY
3✔
539
                                        );
3✔
540
                                }
541
                        } finally {
542
                                fclose($resource);
17✔
543
                        }
544
                }
545
        }
546

547
        /**
548
         * @return resource
549
         * @throws LibresignException
550
         */
551
        protected function getLibreSignFileAsResource() {
552
                $files = $this->getNextcloudFiles($this->libreSignFile);
12✔
553
                if (empty($files)) {
11✔
554
                        throw new LibresignException('File not found');
×
555
                }
556
                $fileToSign = current($files);
11✔
557
                $content = $fileToSign->getContent();
11✔
558
                $resource = fopen('php://memory', 'r+');
11✔
559
                if ($resource === false) {
11✔
560
                        throw new LibresignException('Failed to create temporary resource for PDF validation');
×
561
                }
562
                fwrite($resource, $content);
11✔
563
                rewind($resource);
11✔
564
                return $resource;
11✔
565
        }
566

567
        protected function computeHash(File $file): string {
568
                return hash('sha256', $file->getContent());
2✔
569
        }
570

571
        protected function updateSignRequest(string $hash): void {
572
                $lastSignedDate = $this->getEngine()->getLastSignedDate();
14✔
573
                $this->signRequest->setSigned($lastSignedDate);
14✔
574
                $this->signRequest->setSignedHash($hash);
14✔
575
                $this->signRequest->setStatusEnum(\OCA\Libresign\Enum\SignRequestStatus::SIGNED);
14✔
576

577
                $this->signRequestMapper->update($this->signRequest);
14✔
578

579
                $this->sequentialSigningService
14✔
580
                        ->setFile($this->libreSignFile)
14✔
581
                        ->releaseNextOrder(
14✔
582
                                $this->signRequest->getFileId(),
14✔
583
                                $this->signRequest->getSigningOrder()
14✔
584
                        );
14✔
585
        }
586

587
        protected function updateLibreSignFile(int $nodeId, string $hash): void {
588
                $this->libreSignFile->setSignedNodeId($nodeId);
14✔
589
                $this->libreSignFile->setSignedHash($hash);
14✔
590
                $this->setNewStatusIfNecessary();
14✔
591
                $this->fileMapper->update($this->libreSignFile);
14✔
592

593
                if ($this->libreSignFile->hasParent()) {
14✔
594
                        $this->fileStatusService->propagateStatusToParent($this->libreSignFile->getParentFileId());
×
595
                }
596
        }
597

598
        protected function dispatchSignedEvent(): void {
599
                $event = $this->signedEventFactory->make(
14✔
600
                        $this->signRequest,
14✔
601
                        $this->libreSignFile,
14✔
602
                        $this->getEngine()->getInputFile(),
14✔
603
                );
14✔
604
                $this->eventDispatcher->dispatchTyped($event);
14✔
605
        }
606

607
        protected function identifyEngine(File $file): SignEngineHandler {
608
                return $this->signEngineFactory->resolve($file->getExtension());
10✔
609
        }
610

611
        protected function getSignatureParams(): array {
612
                $certificateData = $this->readCertificate();
15✔
613
                $signatureParams = $this->buildBaseSignatureParams($certificateData);
15✔
614
                $signatureParams = $this->addEmailToSignatureParams($signatureParams, $certificateData);
15✔
615
                $signatureParams = $this->addMetadataToSignatureParams($signatureParams);
15✔
616
                return $signatureParams;
15✔
617
        }
618

619
        private function buildBaseSignatureParams(array $certificateData): array {
620
                return [
15✔
621
                        'DocumentUUID' => $this->libreSignFile?->getUuid(),
15✔
622
                        'IssuerCommonName' => $certificateData['issuer']['CN'] ?? '',
15✔
623
                        'SignerCommonName' => $certificateData['subject']['CN'] ?? '',
15✔
624
                        'LocalSignerTimezone' => $this->dateTimeZone->getTimeZone()->getName(),
15✔
625
                        'LocalSignerSignatureDateTime' => (new DateTime('now', new \DateTimeZone('UTC')))
15✔
626
                                ->format(DateTimeInterface::ATOM)
15✔
627
                ];
15✔
628
        }
629

630
        private function addEmailToSignatureParams(array $signatureParams, array $certificateData): array {
631
                if (isset($certificateData['extensions']['subjectAltName'])) {
15✔
632
                        preg_match('/(?:email:)+(?<email>[^\s,]+)/', $certificateData['extensions']['subjectAltName'], $matches);
6✔
633
                        if ($matches && filter_var($matches['email'], FILTER_VALIDATE_EMAIL)) {
6✔
634
                                $signatureParams['SignerEmail'] = $matches['email'];
4✔
635
                        } elseif (filter_var($certificateData['extensions']['subjectAltName'], FILTER_VALIDATE_EMAIL)) {
2✔
636
                                $signatureParams['SignerEmail'] = $certificateData['extensions']['subjectAltName'];
1✔
637
                        }
638
                }
639
                if (empty($signatureParams['SignerEmail']) && $this->user instanceof IUser) {
15✔
640
                        $signatureParams['SignerEmail'] = $this->user->getEMailAddress();
1✔
641
                }
642
                if (empty($signatureParams['SignerEmail']) && $this->signRequest instanceof SignRequestEntity) {
15✔
643
                        $identifyMethod = $this->identifyMethodService->getIdentifiedMethod($this->signRequest->getId());
9✔
644
                        if ($identifyMethod->getName() === IdentifyMethodService::IDENTIFY_EMAIL) {
9✔
645
                                $signatureParams['SignerEmail'] = $identifyMethod->getEntity()->getIdentifierValue();
1✔
646
                        }
647
                }
648
                return $signatureParams;
15✔
649
        }
650

651
        private function addMetadataToSignatureParams(array $signatureParams): array {
652
                $signRequestMetadata = $this->signRequest->getMetadata();
15✔
653
                if (isset($signRequestMetadata['remote-address'])) {
15✔
654
                        $signatureParams['SignerIP'] = $signRequestMetadata['remote-address'];
2✔
655
                }
656
                if (isset($signRequestMetadata['user-agent'])) {
15✔
657
                        $signatureParams['SignerUserAgent'] = $signRequestMetadata['user-agent'];
2✔
658
                }
659
                return $signatureParams;
15✔
660
        }
661

662
        public function storeUserMetadata(array $metadata = []): self {
663
                $collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
18✔
664
                if (!$collectMetadata || !$metadata) {
18✔
665
                        return $this;
7✔
666
                }
667
                $this->signRequest->setMetadata(array_merge(
11✔
668
                        $this->signRequest->getMetadata() ?? [],
11✔
669
                        $metadata,
11✔
670
                ));
11✔
671
                $this->signRequestMapper->update($this->signRequest);
11✔
672
                return $this;
11✔
673
        }
674

675
        /**
676
         * @return SignRequestEntity[]
677
         */
678
        protected function getSigners(): array {
679
                return $this->signRequestMapper->getByFileId($this->signRequest->getFileId());
×
680
        }
681

682
        protected function setNewStatusIfNecessary(): bool {
683
                $newStatus = $this->evaluateStatusFromSigners();
10✔
684

685
                if ($newStatus === null || $newStatus === $this->libreSignFile->getStatus()) {
10✔
686
                        return false;
4✔
687
                }
688

689
                $this->libreSignFile->setStatus($newStatus);
6✔
690
                return true;
6✔
691
        }
692

693
        private function evaluateStatusFromSigners(): ?int {
694
                $signers = $this->getSigners();
10✔
695

696
                $total = count($signers);
10✔
697

698
                if ($total === 0) {
10✔
699
                        return null;
1✔
700
                }
701

702
                $totalSigned = count(array_filter($signers, fn ($s) => $s->getSigned() !== null));
9✔
703

704
                if ($totalSigned === $total) {
9✔
705
                        return FileStatus::SIGNED->value;
5✔
706
                }
707

708
                if ($totalSigned > 0) {
4✔
709
                        return FileStatus::PARTIAL_SIGNED->value;
3✔
710
                }
711

712
                return null;
1✔
713
        }
714

715
        private function getOrGeneratePfxContent(SignEngineHandler $engine): string {
716
                if ($certificate = $engine->getCertificate()) {
12✔
717
                        return $certificate;
×
718
                }
719
                if ($this->signWithoutPassword) {
12✔
720
                        $tempPassword = $this->generateTemporaryPassword();
1✔
721
                        $this->setPassword($tempPassword);
1✔
722
                        $engine->generateCertificate(
1✔
723
                                [
1✔
724
                                        'host' => $this->userUniqueIdentifier,
1✔
725
                                        'uid' => $this->userUniqueIdentifier,
1✔
726
                                        'name' => $this->friendlyName,
1✔
727
                                ],
1✔
728
                                $tempPassword,
1✔
729
                                $this->friendlyName,
1✔
730
                        );
1✔
731
                }
732
                return $engine->getPfxOfCurrentSigner();
12✔
733
        }
734

735
        private function generateTemporaryPassword(): string {
736
                $passwordEvent = new GenerateSecurePasswordEvent();
1✔
737
                $this->eventDispatcher->dispatchTyped($passwordEvent);
1✔
738
                return $passwordEvent->getPassword() ?? $this->secureRandom->generate(20);
1✔
739
        }
740

741
        protected function readCertificate(): array {
742
                return $this->getEngine()
×
743
                        ->readCertificate();
×
744
        }
745

746
        /**
747
         * Get file to sign
748
         *
749
         * @throws LibresignException
750
         */
751
        protected function getFileToSign(): File {
752
                if ($this->fileToSign instanceof File) {
×
753
                        return $this->fileToSign;
×
754
                }
755

756
                $userId = $this->libreSignFile->getUserId();
×
757
                $nodeId = $this->libreSignFile->getNodeId();
×
758

759
                $originalFile = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
×
760
                if (!$originalFile instanceof File) {
×
761
                        throw new LibresignException($this->l10n->t('File not found'));
×
762
                }
763
                if ($this->isPdf($originalFile)) {
×
764
                        $this->fileToSign = $this->getPdfToSign($originalFile);
×
765
                } else {
766
                        $this->fileToSign = $originalFile;
×
767
                }
768
                return $this->fileToSign;
×
769
        }
770

771
        private function isPdf(File $file): bool {
772
                return strcasecmp($file->getExtension(), 'pdf') === 0;
×
773
        }
774

775
        protected function getEngine(): SignEngineHandler {
776
                if (!$this->engine) {
12✔
777
                        $originalFile = $this->getFileToSign();
12✔
778
                        $this->engine = $this->identifyEngine($originalFile);
12✔
779

780
                        $this->configureEngine();
12✔
781
                }
782
                return $this->engine;
12✔
783
        }
784

785
        private function configureEngine(): void {
786
                $this->engine
12✔
787
                        ->setInputFile($this->getFileToSign())
12✔
788
                        ->setCertificate($this->getOrGeneratePfxContent($this->engine))
12✔
789
                        ->setPassword($this->password);
12✔
790

791
                if ($this->engine::class === Pkcs12Handler::class) {
12✔
792
                        $this->engine
2✔
793
                                ->setVisibleElements($this->getVisibleElements())
2✔
794
                                ->setSignatureParams($this->getSignatureParams());
2✔
795
                }
796
        }
797

798
        public function getLibresignFile(?int $nodeId, ?string $signRequestUuid = null): FileEntity {
799
                try {
800
                        if ($nodeId) {
3✔
801
                                return $this->fileMapper->getByNodeId($nodeId);
1✔
802
                        }
803

804
                        if ($signRequestUuid) {
2✔
805
                                $signRequest = $this->signRequestMapper->getByUuid($signRequestUuid);
2✔
806
                                return $this->fileMapper->getById($signRequest->getFileId());
2✔
807
                        }
808

809
                        throw new \Exception('Invalid arguments');
×
810

811
                } catch (DoesNotExistException) {
1✔
812
                        throw new LibresignException($this->l10n->t('File not found'), 1);
1✔
813
                }
814
        }
815

816
        public function renew(SignRequestEntity $signRequest, string $method): void {
817
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
818
                if (empty($identifyMethods[$method])) {
×
819
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
×
820
                }
821

822
                $signRequest->setUuid(UUIDUtil::getUUID());
×
823
                $this->signRequestMapper->update($signRequest);
×
824

825
                array_map(function (IIdentifyMethod $identifyMethod): void {
×
826
                        $entity = $identifyMethod->getEntity();
×
827
                        $entity->setAttempts($entity->getAttempts() + 1);
×
828
                        $entity->setLastAttemptDate($this->timeFactory->getDateTime());
×
829
                        $identifyMethod->save();
×
830
                }, $identifyMethods[$method]);
×
831
        }
832

833
        public function requestCode(
834
                SignRequestEntity $signRequest,
835
                string $identifyMethodName,
836
                string $signMethodName,
837
                string $identify = '',
838
        ): void {
839
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
840
                if (empty($identifyMethods[$identifyMethodName])) {
×
841
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
×
842
                }
843
                foreach ($identifyMethods[$identifyMethodName] as $identifyMethod) {
×
844
                        try {
845
                                $signatureMethod = $identifyMethod->getEmptyInstanceOfSignatureMethodByName($signMethodName);
×
846
                                $signatureMethod->setEntity($identifyMethod->getEntity());
×
847
                        } catch (InvalidArgumentException) {
×
848
                                continue;
×
849
                        }
850
                        /** @var IToken $signatureMethod */
851
                        $identifier = $identify ?: $identifyMethod->getEntity()->getIdentifierValue();
×
852
                        $signatureMethod->requestCode($identifier, $identifyMethod->getEntity()->getIdentifierKey());
×
853
                        return;
×
854
                }
855
                throw new LibresignException($this->l10n->t('Sending authorization code not enabled.'));
×
856
        }
857

858
        public function getSignRequestToSign(FileEntity $libresignFile, ?string $signRequestUuid, ?IUser $user): SignRequestEntity {
859
                $this->validateHelper->fileCanBeSigned($libresignFile);
2✔
860
                try {
861
                        if ($libresignFile->isEnvelope()) {
2✔
862
                                $childFiles = $this->fileMapper->getChildrenFiles($libresignFile->getId());
×
863
                                $allSignRequests = [];
×
864
                                foreach ($childFiles as $childFile) {
×
865
                                        $childSignRequests = $this->signRequestMapper->getByFileId($childFile->getId());
×
866
                                        $allSignRequests = array_merge($allSignRequests, $childSignRequests);
×
867
                                }
868
                                $signRequests = $allSignRequests;
×
869
                        } else {
870
                                $signRequests = $this->signRequestMapper->getByFileId($libresignFile->getId());
2✔
871
                        }
872

873
                        if (!empty($signRequestUuid)) {
2✔
874
                                $signRequest = $this->getSignRequestByUuid($signRequestUuid);
2✔
875
                        } else {
876
                                $signRequest = array_reduce($signRequests, function (?SignRequestEntity $carry, SignRequestEntity $signRequest) use ($user): ?SignRequestEntity {
×
877
                                        $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
878
                                        $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($user) {
×
879
                                                if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL
×
880
                                                        && $user
881
                                                        && (
882
                                                                $identifyMethod->getIdentifierValue() === $user->getUID()
×
883
                                                                || $identifyMethod->getIdentifierValue() === $user->getEMailAddress()
×
884
                                                        )
885
                                                ) {
886
                                                        return true;
×
887
                                                }
888
                                                if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT
×
889
                                                        && $user
890
                                                        && $identifyMethod->getIdentifierValue() === $user->getUID()
×
891
                                                ) {
892
                                                        return true;
×
893
                                                }
894
                                                return false;
×
895
                                        });
×
896
                                        if (count($found) > 0) {
×
897
                                                return $signRequest;
×
898
                                        }
899
                                        return $carry;
×
900
                                });
×
901
                        }
902

903
                        if (!$signRequest) {
2✔
904
                                throw new DoesNotExistException('Sign request not found');
×
905
                        }
906
                        if ($signRequest->getSigned()) {
2✔
907
                                throw new LibresignException($this->l10n->t('File already signed by you'), 1);
×
908
                        }
909
                        return $signRequest;
2✔
910
                } catch (DoesNotExistException) {
×
911
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
912
                }
913
        }
914

915
        protected function getPdfToSign(File $originalFile): File {
916
                $file = $this->getSignedFile();
×
917
                if ($file instanceof File) {
×
918
                        return $file;
×
919
                }
920

921
                $originalContent = $originalFile->getContent();
×
922

923
                if ($this->pdfSignatureDetectionService->hasSignatures($originalContent)) {
×
924
                        return $this->createSignedFile($originalFile, $originalContent);
×
925
                }
926
                $metadata = $this->footerHandler->getMetadata($originalFile, $this->libreSignFile);
×
927
                $footer = $this->footerHandler
×
928
                        ->setTemplateVar('uuid', $this->libreSignFile->getUuid())
×
929
                        ->setTemplateVar('signers', array_map(fn (SignRequestEntity $signer) => [
×
930
                                'displayName' => $signer->getDisplayName(),
×
931
                                'signed' => $signer->getSigned()
×
932
                                        ? $signer->getSigned()->format(DateTimeInterface::ATOM)
×
933
                                        : null,
934
                        ], $this->getSigners()))
×
935
                        ->getFooter($metadata['d']);
×
936
                if ($footer) {
×
937
                        $stamp = $this->tempManager->getTemporaryFile('stamp.pdf');
×
938
                        file_put_contents($stamp, $footer);
×
939

940
                        $input = $this->tempManager->getTemporaryFile('input.pdf');
×
941
                        file_put_contents($input, $originalContent);
×
942

943
                        try {
944
                                $pdfContent = $this->pdf->applyStamp($input, $stamp);
×
945
                        } catch (RuntimeException $e) {
×
946
                                throw new LibresignException($e->getMessage());
×
947
                        }
948
                } else {
949
                        $pdfContent = $originalContent;
×
950
                }
951
                return $this->createSignedFile($originalFile, $pdfContent);
×
952
        }
953

954
        protected function getSignedFile(): ?File {
955
                $nodeId = $this->libreSignFile->getSignedNodeId();
3✔
956
                if (!$nodeId) {
3✔
957
                        return null;
1✔
958
                }
959

960
                $fileToSign = $this->getNodeByIdUsingUid($this->libreSignFile->getUserId(), $nodeId);
2✔
961

962
                if ($fileToSign->getOwner()->getUID() !== $this->libreSignFile->getUserId()) {
2✔
963
                        $fileToSign = $this->getNodeByIdUsingUid($fileToSign->getOwner()->getUID(), $nodeId);
1✔
964
                }
965
                return $fileToSign;
2✔
966
        }
967

968
        protected function getNodeByIdUsingUid(string $uid, int $nodeId): File {
969
                try {
970
                        $fileToSign = $this->root->getUserFolder($uid)->getFirstNodeById($nodeId);
4✔
971
                } catch (NoUserException) {
2✔
972
                        throw new LibresignException($this->l10n->t('User not found.'));
1✔
973
                } catch (NotPermittedException) {
1✔
974
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
975
                }
976
                if (!$fileToSign instanceof File) {
2✔
977
                        throw new LibresignException($this->l10n->t('File not found'));
1✔
978
                }
979
                return $fileToSign;
1✔
980
        }
981

982
        private function cleanupUnsignedSignedFile(): void {
983
                if (!$this->createdSignedFile instanceof File) {
×
984
                        return;
×
985
                }
986

987
                try {
988
                        $this->createdSignedFile->delete();
×
989
                } catch (\Throwable $e) {
×
990
                        $this->logger->warning('Failed to delete temporary signed file: ' . $e->getMessage());
×
991
                } finally {
992
                        $this->createdSignedFile = null;
×
993
                }
994
        }
995

996
        private function createSignedFile(File $originalFile, string $content): File {
997
                $filename = preg_replace(
×
998
                        '/' . $originalFile->getExtension() . '$/',
×
999
                        $this->l10n->t('signed') . '.' . $originalFile->getExtension(),
×
1000
                        basename($originalFile->getPath())
×
1001
                );
×
1002
                $owner = $originalFile->getOwner()->getUID();
×
1003
                try {
1004
                        /** @var \OCP\Files\Folder */
1005
                        $parentFolder = $this->root->getUserFolder($owner)->getFirstNodeById($originalFile->getParentId());
×
1006
                        $this->createdSignedFile = $parentFolder->newFile($filename, $content);
×
1007
                        return $this->createdSignedFile;
×
1008
                } catch (NotPermittedException) {
×
1009
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
×
1010
                }
1011
        }
1012

1013
        /**
1014
         * @throws DoesNotExistException
1015
         */
1016
        public function getSignRequestByUuid(string $uuid): SignRequestEntity {
1017
                $this->validateHelper->validateUuidFormat($uuid);
4✔
1018
                return $this->signRequestMapper->getByUuid($uuid);
3✔
1019
        }
1020

1021
        /**
1022
         * @throws DoesNotExistException
1023
         */
1024
        public function getFile(int $signRequestId): FileEntity {
1025
                return $this->fileMapper->getById($signRequestId);
×
1026
        }
1027

1028
        /**
1029
         * @throws DoesNotExistException
1030
         */
1031
        public function getFileByUuid(string $uuid): FileEntity {
1032
                return $this->fileMapper->getByUuid($uuid);
×
1033
        }
1034

1035
        public function getIdDocById(int $fileId): IdDocs {
1036
                return $this->idDocsMapper->getByFileId($fileId);
×
1037
        }
1038

1039
        /**
1040
         * @return File[] Array of files
1041
         */
1042
        public function getNextcloudFiles(FileEntity $fileData): array {
1043
                if ($fileData->getNodeType() === 'envelope') {
1✔
1044
                        $children = $this->fileMapper->getChildrenFiles($fileData->getId());
×
1045
                        $files = [];
×
1046
                        foreach ($children as $child) {
×
1047
                                $nodeId = $child->getNodeId();
×
1048
                                if ($nodeId === null) {
×
1049
                                        throw new LibresignException(json_encode([
×
1050
                                                'action' => JSActions::ACTION_DO_NOTHING,
×
1051
                                                'errors' => [['message' => $this->l10n->t('File not found')]],
×
1052
                                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
×
1053
                                }
1054
                                $file = $this->root->getUserFolder($child->getUserId())->getFirstNodeById($nodeId);
×
1055
                                if ($file instanceof File) {
×
1056
                                        $files[] = $file;
×
1057
                                }
1058
                        }
1059
                        return $files;
×
1060
                }
1061

1062
                $nodeId = $fileData->getNodeId();
1✔
1063
                if ($nodeId === null) {
1✔
1064
                        throw new LibresignException(json_encode([
1✔
1065
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
1066
                                'errors' => [['message' => $this->l10n->t('File not found')]],
1✔
1067
                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
1✔
1068
                }
1069
                $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($nodeId);
×
1070
                if (!$fileToSign instanceof File) {
×
1071
                        throw new LibresignException(json_encode([
×
1072
                                'action' => JSActions::ACTION_DO_NOTHING,
×
1073
                                'errors' => [['message' => $this->l10n->t('File not found')]],
×
1074
                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
×
1075
                }
1076
                return [$fileToSign];
×
1077
        }
1078

1079
        /**
1080
         * @return array<FileEntity>
1081
         */
1082
        public function getNextcloudFilesWithEntities(FileEntity $fileData): array {
1083
                if ($fileData->getNodeType() === 'envelope') {
×
1084
                        $children = $this->fileMapper->getChildrenFiles($fileData->getId());
×
1085
                        $result = [];
×
1086
                        foreach ($children as $child) {
×
1087
                                $nodeId = $child->getNodeId();
×
1088
                                if ($nodeId === null) {
×
1089
                                        throw new LibresignException(json_encode([
×
1090
                                                'action' => JSActions::ACTION_DO_NOTHING,
×
1091
                                                'errors' => [['message' => $this->l10n->t('File not found')]],
×
1092
                                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
×
1093
                                }
1094
                                $file = $this->root->getUserFolder($child->getUserId())->getFirstNodeById($nodeId);
×
1095
                                if ($file instanceof File) {
×
1096
                                        $result[] = $child;
×
1097
                                }
1098
                        }
1099
                        return $result;
×
1100
                }
1101

1102
                $nodeId = $fileData->getNodeId();
×
1103
                if ($nodeId === null) {
×
1104
                        throw new LibresignException(json_encode([
×
1105
                                'action' => JSActions::ACTION_DO_NOTHING,
×
1106
                                'errors' => [['message' => $this->l10n->t('File not found')]],
×
1107
                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
×
1108
                }
1109
                $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($nodeId);
×
1110
                if (!$fileToSign instanceof File) {
×
1111
                        throw new LibresignException(json_encode([
×
1112
                                'action' => JSActions::ACTION_DO_NOTHING,
×
1113
                                'errors' => [['message' => $this->l10n->t('File not found')]],
×
1114
                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
×
1115
                }
1116
                return [$fileData];
×
1117
        }
1118

1119
        public function validateSigner(string $uuid, ?IUser $user = null): void {
1120
                $this->validateHelper->validateSigner($uuid, $user);
×
1121
        }
1122

1123
        public function validateRenewSigner(string $uuid, ?IUser $user = null): void {
1124
                $this->validateHelper->validateRenewSigner($uuid, $user);
×
1125
        }
1126

1127
        public function getSignerData(?IUser $user, ?SignRequestEntity $signRequest = null): array {
1128
                $return = ['user' => ['name' => null]];
×
1129
                if ($signRequest) {
×
1130
                        $return['user']['name'] = $signRequest->getDisplayName();
×
1131
                } elseif ($user) {
×
1132
                        $return['user']['name'] = $user->getDisplayName();
×
1133
                }
1134
                return $return;
×
1135
        }
1136

1137
        public function getAvailableIdentifyMethodsFromSettings(): array {
1138
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsSettings();
×
1139
                $return = array_map(fn (array $identifyMethod): array => [
×
1140
                        'mandatory' => $identifyMethod['mandatory'],
×
1141
                        'identifiedAtDate' => null,
×
1142
                        'validateCode' => false,
×
1143
                        'method' => $identifyMethod['name'],
×
1144
                ], $identifyMethods);
×
1145
                return $return;
×
1146
        }
1147

1148
        public function getFileUrl(int $fileId, string $uuid): string {
1149
                try {
1150
                        $this->idDocsMapper->getByFileId($fileId);
×
1151
                        return $this->urlGenerator->linkToRoute('libresign.page.getPdf', ['uuid' => $uuid]);
×
1152
                } catch (DoesNotExistException) {
×
1153
                        return $this->urlGenerator->linkToRoute('libresign.page.getPdfFile', ['uuid' => $uuid]);
×
1154
                }
1155
        }
1156

1157
        /**
1158
         * Get PDF URLs for signing
1159
         * For envelopes: returns URLs for all child files
1160
         * For regular files: returns URL for the file itself
1161
         *
1162
         * @return string[]
1163
         */
1164
        public function getPdfUrlsForSigning(FileEntity $fileEntity, SignRequestEntity $signRequestEntity): array {
1165
                if (!$fileEntity->isEnvelope()) {
×
1166
                        return [
×
1167
                                $this->getFileUrl($fileEntity->getId(), $signRequestEntity->getUuid())
×
1168
                        ];
×
1169
                }
1170

1171
                $childSignRequests = $this->signRequestMapper->getByEnvelopeChildrenAndIdentifyMethod(
×
1172
                        $fileEntity->getId(),
×
1173
                        $signRequestEntity->getId()
×
1174
                );
×
1175

1176
                $pdfUrls = [];
×
1177
                foreach ($childSignRequests as $childSignRequest) {
×
1178
                        $pdfUrls[] = $this->getFileUrl(
×
1179
                                $childSignRequest->getFileId(),
×
1180
                                $childSignRequest->getUuid()
×
1181
                        );
×
1182
                }
1183

1184
                return $pdfUrls;
×
1185
        }
1186

1187
        private function recordSignatureAttempt(Exception $exception): void {
1188
                if (!$this->libreSignFile) {
×
1189
                        return;
×
1190
                }
1191

1192
                $metadata = $this->libreSignFile->getMetadata() ?? [];
×
1193

1194
                if (!isset($metadata['signature_attempts'])) {
×
1195
                        $metadata['signature_attempts'] = [];
×
1196
                }
1197

1198
                $attempt = [
×
1199
                        'timestamp' => (new DateTime())->format(\DateTime::ATOM),
×
1200
                        'engine' => get_class($this->engine),
×
1201
                        'error_message' => $exception->getMessage(),
×
1202
                        'error_code' => $exception->getCode(),
×
1203
                ];
×
1204

1205
                $metadata['signature_attempts'][] = $attempt;
×
1206
                $this->libreSignFile->setMetadata($metadata);
×
1207
                $this->fileMapper->update($this->libreSignFile);
×
1208
        }
1209
}
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