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

LibreSign / libresign / 20213135400

14 Dec 2025 07:38PM UTC coverage: 44.224%. First build
20213135400

Pull #6187

github

web-flow
Merge 50b66c4f7 into 7a542b3a8
Pull Request #6187: feat: docmdp per file

17 of 26 new or added lines in 5 files covered. (65.38%)

5961 of 13479 relevant lines covered (44.22%)

5.12 hits per line

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

60.53
/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\Events\SignedEventFactory;
31
use OCA\Libresign\Exception\LibresignException;
32
use OCA\Libresign\Handler\DocMdpHandler;
33
use OCA\Libresign\Handler\FooterHandler;
34
use OCA\Libresign\Handler\PdfTk\Pdf;
35
use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
36
use OCA\Libresign\Handler\SignEngine\SignEngineFactory;
37
use OCA\Libresign\Handler\SignEngine\SignEngineHandler;
38
use OCA\Libresign\Helper\JSActions;
39
use OCA\Libresign\Helper\ValidateHelper;
40
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
41
use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\IToken;
42
use OCP\AppFramework\Db\DoesNotExistException;
43
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
44
use OCP\AppFramework\Utility\ITimeFactory;
45
use OCP\EventDispatcher\IEventDispatcher;
46
use OCP\Files\File;
47
use OCP\Files\IRootFolder;
48
use OCP\Files\NotPermittedException;
49
use OCP\Http\Client\IClientService;
50
use OCP\IAppConfig;
51
use OCP\IDateTimeZone;
52
use OCP\IL10N;
53
use OCP\ITempManager;
54
use OCP\IURLGenerator;
55
use OCP\IUser;
56
use OCP\IUserManager;
57
use OCP\IUserSession;
58
use OCP\Security\Events\GenerateSecurePasswordEvent;
59
use OCP\Security\ISecureRandom;
60
use Psr\Log\LoggerInterface;
61
use RuntimeException;
62
use Sabre\DAV\UUIDUtil;
63

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

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

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

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

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

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

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

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

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

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

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

216
        public function setVisibleElements(array $list): self {
217
                $fileElements = $this->fileElementMapper->getByFileIdAndSignRequestId($this->signRequest->getFileId(), $this->signRequest->getId());
14✔
218
                $canCreateSignature = $this->signerElementsService->canCreateSignature();
14✔
219

220
                foreach ($fileElements as $fileElement) {
14✔
221
                        $this->elements[] = $this->buildVisibleElementAssoc($fileElement, $list, $canCreateSignature);
12✔
222
                }
223

224
                return $this;
5✔
225
        }
226

227
        private function buildVisibleElementAssoc(FileElement $fileElement, array $list, bool $canCreateSignature): VisibleElementAssoc {
228
                if (!$canCreateSignature) {
12✔
229
                        return new VisibleElementAssoc($fileElement);
1✔
230
                }
231

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

235
                return $this->bindFileElementWithTempFile($fileElement, $nodeId);
7✔
236
        }
237

238
        private function getNodeId(?array $element, FileElement $fileElement): int {
239
                if ($this->isValidElement($element)) {
11✔
240
                        return (int)$element['profileNodeId'];
7✔
241
                }
242

243
                return $this->retrieveUserElement($fileElement);
×
244
        }
245

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

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

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

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

289
        private function getNode(int $nodeId): ?File {
290
                if ($this->user instanceof IUser) {
7✔
291
                        return $this->folderService->getFileById($nodeId);
6✔
292
                }
293

294
                $filesOfElementes = $this->signerElementsService->getElementsFromSession();
1✔
295
                return $this->array_find($filesOfElementes, fn ($file) => $file->getId() === $nodeId);
1✔
296
        }
297

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

313
                return null;
3✔
314
        }
315

316
        /**
317
         * @return VisibleElementAssoc[]
318
         */
319
        public function getVisibleElements(): array {
320
                return $this->elements;
7✔
321
        }
322

323
        public function sign(): File {
324
                $this->validateDocMdpAllowsSignatures();
18✔
325
                $signedFile = $this->getEngine()->sign();
16✔
326

327
                $hash = $this->computeHash($signedFile);
16✔
328

329
                $this->updateSignRequest($hash);
16✔
330
                $this->updateLibreSignFile($hash);
16✔
331

332
                $this->dispatchSignedEvent();
16✔
333

334
                return $signedFile;
16✔
335
        }
336

337
        /**
338
         * @throws LibresignException If the document has DocMDP level 1 (no changes allowed)
339
         */
340
        protected function validateDocMdpAllowsSignatures(): void {
341
                $docmdpLevel = $this->libreSignFile->getDocmdpLevelEnum();
18✔
342

343
                if ($docmdpLevel === \OCA\Libresign\Enum\DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED) {
18✔
NEW
344
                        throw new LibresignException(
×
NEW
345
                                $this->l10n->t('This document has been certified with no changes allowed. You cannot add more signers to this document.'),
×
NEW
346
                                AppFrameworkHttp::STATUS_UNPROCESSABLE_ENTITY
×
NEW
347
                        );
×
348
                }
349

350
                if ($docmdpLevel === \OCA\Libresign\Enum\DocMdpLevel::NOT_CERTIFIED) {
18✔
351
                        $resource = $this->getLibreSignFileAsResource();
18✔
352

353
                        try {
354
                                if (!$this->docMdpHandler->allowsAdditionalSignatures($resource)) {
17✔
355
                                        throw new LibresignException(
3✔
356
                                                $this->l10n->t('This document has been certified with no changes allowed. You cannot add more signers to this document.'),
3✔
357
                                                AppFrameworkHttp::STATUS_UNPROCESSABLE_ENTITY
3✔
358
                                        );
3✔
359
                                }
360
                        } finally {
361
                                fclose($resource);
17✔
362
                        }
363
                }
364
        }
365

366
        /**
367
         * @return resource
368
         * @throws LibresignException
369
         */
370
        protected function getLibreSignFileAsResource() {
371
                $fileToSign = $this->getNextcloudFile($this->libreSignFile);
12✔
372
                $content = $fileToSign->getContent();
11✔
373
                $resource = fopen('php://memory', 'r+');
11✔
374
                if ($resource === false) {
11✔
375
                        throw new LibresignException('Failed to create temporary resource for PDF validation');
×
376
                }
377
                fwrite($resource, $content);
11✔
378
                rewind($resource);
11✔
379
                return $resource;
11✔
380
        }
381

382
        protected function computeHash(File $file): string {
383
                return hash('sha256', $file->getContent());
2✔
384
        }
385

386
        protected function updateSignRequest(string $hash): void {
387
                $lastSignedDate = $this->getEngine()->getLastSignedDate();
14✔
388
                $this->signRequest->setSigned($lastSignedDate);
14✔
389
                $this->signRequest->setSignedHash($hash);
14✔
390
                $this->signRequest->setStatusEnum(\OCA\Libresign\Enum\SignRequestStatus::SIGNED);
14✔
391

392
                $this->signRequestMapper->update($this->signRequest);
14✔
393

394
                $this->sequentialSigningService
14✔
395
                        ->setFile($this->libreSignFile)
14✔
396
                        ->releaseNextOrder(
14✔
397
                                $this->signRequest->getFileId(),
14✔
398
                                $this->signRequest->getSigningOrder()
14✔
399
                        );
14✔
400
        }
401

402
        protected function updateLibreSignFile(string $hash): void {
403
                $nodeId = $this->getEngine()->getInputFile()->getId();
14✔
404
                $this->libreSignFile->setSignedNodeId($nodeId);
14✔
405
                $this->libreSignFile->setSignedHash($hash);
14✔
406
                $this->setNewStatusIfNecessary();
14✔
407
                $this->fileMapper->update($this->libreSignFile);
14✔
408
        }
409

410
        protected function dispatchSignedEvent(): void {
411
                $event = $this->signedEventFactory->make(
14✔
412
                        $this->signRequest,
14✔
413
                        $this->libreSignFile,
14✔
414
                        $this->getEngine()->getInputFile(),
14✔
415
                );
14✔
416
                $this->eventDispatcher->dispatchTyped($event);
14✔
417
        }
418

419
        protected function identifyEngine(File $file): SignEngineHandler {
420
                return $this->signEngineFactory->resolve($file->getExtension());
10✔
421
        }
422

423
        protected function getSignatureParams(): array {
424
                $certificateData = $this->readCertificate();
15✔
425
                $signatureParams = $this->buildBaseSignatureParams($certificateData);
15✔
426
                $signatureParams = $this->addEmailToSignatureParams($signatureParams, $certificateData);
15✔
427
                $signatureParams = $this->addMetadataToSignatureParams($signatureParams);
15✔
428
                return $signatureParams;
15✔
429
        }
430

431
        private function buildBaseSignatureParams(array $certificateData): array {
432
                return [
15✔
433
                        'DocumentUUID' => $this->libreSignFile?->getUuid(),
15✔
434
                        'IssuerCommonName' => $certificateData['issuer']['CN'] ?? '',
15✔
435
                        'SignerCommonName' => $certificateData['subject']['CN'] ?? '',
15✔
436
                        'LocalSignerTimezone' => $this->dateTimeZone->getTimeZone()->getName(),
15✔
437
                        'LocalSignerSignatureDateTime' => (new DateTime('now', new \DateTimeZone('UTC')))
15✔
438
                                ->format(DateTimeInterface::ATOM)
15✔
439
                ];
15✔
440
        }
441

442
        private function addEmailToSignatureParams(array $signatureParams, array $certificateData): array {
443
                if (isset($certificateData['extensions']['subjectAltName'])) {
15✔
444
                        preg_match('/(?:email:)+(?<email>[^\s,]+)/', $certificateData['extensions']['subjectAltName'], $matches);
6✔
445
                        if ($matches && filter_var($matches['email'], FILTER_VALIDATE_EMAIL)) {
6✔
446
                                $signatureParams['SignerEmail'] = $matches['email'];
4✔
447
                        } elseif (filter_var($certificateData['extensions']['subjectAltName'], FILTER_VALIDATE_EMAIL)) {
2✔
448
                                $signatureParams['SignerEmail'] = $certificateData['extensions']['subjectAltName'];
1✔
449
                        }
450
                }
451
                if (empty($signatureParams['SignerEmail']) && $this->user instanceof IUser) {
15✔
452
                        $signatureParams['SignerEmail'] = $this->user->getEMailAddress();
1✔
453
                }
454
                if (empty($signatureParams['SignerEmail'])) {
15✔
455
                        $identifyMethod = $this->identifyMethodService->getIdentifiedMethod($this->signRequest->getId());
9✔
456
                        if ($identifyMethod->getName() === IdentifyMethodService::IDENTIFY_EMAIL) {
9✔
457
                                $signatureParams['SignerEmail'] = $identifyMethod->getEntity()->getIdentifierValue();
1✔
458
                        }
459
                }
460
                return $signatureParams;
15✔
461
        }
462

463
        private function addMetadataToSignatureParams(array $signatureParams): array {
464
                $signRequestMetadata = $this->signRequest->getMetadata();
15✔
465
                if (isset($signRequestMetadata['remote-address'])) {
15✔
466
                        $signatureParams['SignerIP'] = $signRequestMetadata['remote-address'];
2✔
467
                }
468
                if (isset($signRequestMetadata['user-agent'])) {
15✔
469
                        $signatureParams['SignerUserAgent'] = $signRequestMetadata['user-agent'];
2✔
470
                }
471
                return $signatureParams;
15✔
472
        }
473

474
        public function storeUserMetadata(array $metadata = []): self {
475
                $collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
18✔
476
                if (!$collectMetadata || !$metadata) {
18✔
477
                        return $this;
7✔
478
                }
479
                $this->signRequest->setMetadata(array_merge(
11✔
480
                        $this->signRequest->getMetadata() ?? [],
11✔
481
                        $metadata,
11✔
482
                ));
11✔
483
                $this->signRequestMapper->update($this->signRequest);
11✔
484
                return $this;
11✔
485
        }
486

487
        /**
488
         * @return SignRequestEntity[]
489
         */
490
        protected function getSigners(): array {
491
                if (empty($this->signers)) {
×
492
                        $this->signers = $this->signRequestMapper->getByFileId($this->signRequest->getFileId());
×
493
                        if ($this->signers) {
×
494
                                foreach ($this->signers as $key => $signer) {
×
495
                                        if ($signer->getId() === $this->signRequest->getId()) {
×
496
                                                $this->signers[$key] = $this->signRequest;
×
497
                                                break;
×
498
                                        }
499
                                }
500
                        }
501
                }
502
                return $this->signers;
×
503
        }
504

505
        protected function setNewStatusIfNecessary(): bool {
506
                $newStatus = $this->evaluateStatusFromSigners();
10✔
507

508
                if ($newStatus === null || $newStatus === $this->libreSignFile->getStatus()) {
10✔
509
                        return false;
4✔
510
                }
511

512
                $this->libreSignFile->setStatus($newStatus);
6✔
513
                return true;
6✔
514
        }
515

516
        private function evaluateStatusFromSigners(): ?int {
517
                $signers = $this->getSigners();
10✔
518

519
                $total = count($signers);
10✔
520

521
                if ($total === 0) {
10✔
522
                        return null;
1✔
523
                }
524

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

527
                if ($totalSigned === $total) {
9✔
528
                        return FileEntity::STATUS_SIGNED;
5✔
529
                }
530

531
                if ($totalSigned > 0) {
4✔
532
                        return FileEntity::STATUS_PARTIAL_SIGNED;
3✔
533
                }
534

535
                return null;
1✔
536
        }
537

538
        private function getOrGeneratePfxContent(SignEngineHandler $engine): string {
539
                if ($certificate = $engine->getCertificate()) {
12✔
540
                        return $certificate;
×
541
                }
542
                if ($this->signWithoutPassword) {
12✔
543
                        $tempPassword = $this->generateTemporaryPassword();
1✔
544
                        $this->setPassword($tempPassword);
1✔
545
                        $engine->generateCertificate(
1✔
546
                                [
1✔
547
                                        'host' => $this->userUniqueIdentifier,
1✔
548
                                        'uid' => $this->userUniqueIdentifier,
1✔
549
                                        'name' => $this->friendlyName,
1✔
550
                                ],
1✔
551
                                $tempPassword,
1✔
552
                                $this->friendlyName,
1✔
553
                        );
1✔
554
                }
555
                return $engine->getPfxOfCurrentSigner();
12✔
556
        }
557

558
        private function generateTemporaryPassword(): string {
559
                $passwordEvent = new GenerateSecurePasswordEvent();
1✔
560
                $this->eventDispatcher->dispatchTyped($passwordEvent);
1✔
561
                return $passwordEvent->getPassword() ?? $this->secureRandom->generate(20);
1✔
562
        }
563

564
        protected function readCertificate(): array {
565
                return $this->getEngine()
×
566
                        ->readCertificate();
×
567
        }
568

569
        /**
570
         * Get file to sign
571
         *
572
         * @throws LibresignException
573
         */
574
        protected function getFileToSign(): File {
575
                if ($this->fileToSign instanceof File) {
×
576
                        return $this->fileToSign;
×
577
                }
578

579
                $userId = $this->libreSignFile->getUserId();
×
580
                $nodeId = $this->libreSignFile->getNodeId();
×
581

582
                $originalFile = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
×
583
                if (!$originalFile instanceof File) {
×
584
                        throw new LibresignException($this->l10n->t('File not found'));
×
585
                }
586
                if ($this->isPdf($originalFile)) {
×
587
                        $this->fileToSign = $this->getPdfToSign($originalFile);
×
588
                } else {
589
                        $this->fileToSign = $originalFile;
×
590
                }
591
                return $this->fileToSign;
×
592
        }
593

594
        private function isPdf(File $file): bool {
595
                return strcasecmp($file->getExtension(), 'pdf') === 0;
×
596
        }
597

598
        protected function getEngine(): SignEngineHandler {
599
                if (!$this->engine) {
12✔
600
                        $originalFile = $this->getFileToSign();
12✔
601
                        $this->engine = $this->identifyEngine($originalFile);
12✔
602

603
                        $this->configureEngine();
12✔
604
                }
605
                return $this->engine;
12✔
606
        }
607

608
        private function configureEngine(): void {
609
                $this->engine
12✔
610
                        ->setInputFile($this->getFileToSign())
12✔
611
                        ->setCertificate($this->getOrGeneratePfxContent($this->engine))
12✔
612
                        ->setPassword($this->password);
12✔
613

614
                if ($this->engine::class === Pkcs12Handler::class) {
12✔
615
                        $this->engine
2✔
616
                                ->setVisibleElements($this->getVisibleElements())
2✔
617
                                ->setSignatureParams($this->getSignatureParams());
2✔
618
                }
619
        }
620

621
        public function getLibresignFile(?int $nodeId, ?string $signRequestUuid = null): FileEntity {
622
                try {
623
                        if ($nodeId) {
3✔
624
                                return $this->fileMapper->getByFileId($nodeId);
1✔
625
                        }
626

627
                        if ($signRequestUuid) {
2✔
628
                                $signRequest = $this->signRequestMapper->getByUuid($signRequestUuid);
2✔
629
                                return $this->fileMapper->getById($signRequest->getFileId());
2✔
630
                        }
631

632
                        throw new \Exception('Invalid arguments');
×
633

634
                } catch (DoesNotExistException) {
1✔
635
                        throw new LibresignException($this->l10n->t('File not found'), 1);
1✔
636
                }
637
        }
638

639
        public function renew(SignRequestEntity $signRequest, string $method): void {
640
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
641
                if (empty($identifyMethods[$method])) {
×
642
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
×
643
                }
644

645
                $signRequest->setUuid(UUIDUtil::getUUID());
×
646
                $this->signRequestMapper->update($signRequest);
×
647

648
                array_map(function (IIdentifyMethod $identifyMethod): void {
×
649
                        $entity = $identifyMethod->getEntity();
×
650
                        $entity->setAttempts($entity->getAttempts() + 1);
×
651
                        $entity->setLastAttemptDate($this->timeFactory->getDateTime());
×
652
                        $identifyMethod->save();
×
653
                }, $identifyMethods[$method]);
×
654
        }
655

656
        public function requestCode(
657
                SignRequestEntity $signRequest,
658
                string $identifyMethodName,
659
                string $signMethodName,
660
                string $identify = '',
661
        ): void {
662
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
663
                if (empty($identifyMethods[$identifyMethodName])) {
×
664
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
×
665
                }
666
                foreach ($identifyMethods[$identifyMethodName] as $identifyMethod) {
×
667
                        try {
668
                                $signatureMethod = $identifyMethod->getEmptyInstanceOfSignatureMethodByName($signMethodName);
×
669
                                $signatureMethod->setEntity($identifyMethod->getEntity());
×
670
                        } catch (InvalidArgumentException) {
×
671
                                continue;
×
672
                        }
673
                        /** @var IToken $signatureMethod */
674
                        $identifier = $identify ?: $identifyMethod->getEntity()->getIdentifierValue();
×
675
                        $signatureMethod->requestCode($identifier, $identifyMethod->getEntity()->getIdentifierKey());
×
676
                        return;
×
677
                }
678
                throw new LibresignException($this->l10n->t('Sending authorization code not enabled.'));
×
679
        }
680

681
        public function getSignRequestToSign(FileEntity $libresignFile, ?string $signRequestUuid, ?IUser $user): SignRequestEntity {
682
                $this->validateHelper->fileCanBeSigned($libresignFile);
2✔
683
                try {
684
                        $signRequests = $this->signRequestMapper->getByFileId($libresignFile->getId());
2✔
685

686
                        if (!empty($signRequestUuid)) {
2✔
687
                                $signRequest = $this->getSignRequestByUuid($signRequestUuid);
2✔
688
                        } else {
689
                                $signRequest = array_reduce($signRequests, function (?SignRequestEntity $carry, SignRequestEntity $signRequest) use ($user): ?SignRequestEntity {
×
690
                                        $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
691
                                        $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($user) {
×
692
                                                if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL
×
693
                                                        && $user
694
                                                        && (
695
                                                                $identifyMethod->getIdentifierValue() === $user->getUID()
×
696
                                                                || $identifyMethod->getIdentifierValue() === $user->getEMailAddress()
×
697
                                                        )
698
                                                ) {
699
                                                        return true;
×
700
                                                }
701
                                                if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT
×
702
                                                        && $user
703
                                                        && $identifyMethod->getIdentifierValue() === $user->getUID()
×
704
                                                ) {
705
                                                        return true;
×
706
                                                }
707
                                                return false;
×
708
                                        });
×
709
                                        if (count($found) > 0) {
×
710
                                                return $signRequest;
×
711
                                        }
712
                                        return $carry;
×
713
                                });
×
714
                        }
715

716
                        if (!$signRequest) {
2✔
717
                                throw new DoesNotExistException('Sign request not found');
×
718
                        }
719
                        if ($signRequest->getSigned()) {
2✔
720
                                throw new LibresignException($this->l10n->t('File already signed by you'), 1);
×
721
                        }
722
                        return $signRequest;
2✔
723
                } catch (DoesNotExistException) {
×
724
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
725
                }
726
        }
727

728
        protected function getPdfToSign(File $originalFile): File {
729
                $file = $this->getSignedFile();
×
730
                if ($file instanceof File) {
×
731
                        return $file;
×
732
                }
733

734
                $originalContent = $originalFile->getContent();
×
735

736
                if ($this->pdfSignatureDetectionService->hasSignatures($originalContent)) {
×
737
                        return $this->createSignedFile($originalFile, $originalContent);
×
738
                }
739
                $metadata = $this->footerHandler->getMetadata($originalFile, $this->libreSignFile);
×
740
                $footer = $this->footerHandler
×
741
                        ->setTemplateVar('uuid', $this->libreSignFile->getUuid())
×
742
                        ->setTemplateVar('signers', array_map(fn (SignRequestEntity $signer) => [
×
743
                                'displayName' => $signer->getDisplayName(),
×
744
                                'signed' => $signer->getSigned()
×
745
                                        ? $signer->getSigned()->format(DateTimeInterface::ATOM)
×
746
                                        : null,
747
                        ], $this->getSigners()))
×
748
                        ->getFooter($metadata['d']);
×
749
                if ($footer) {
×
750
                        $stamp = $this->tempManager->getTemporaryFile('stamp.pdf');
×
751
                        file_put_contents($stamp, $footer);
×
752

753
                        $input = $this->tempManager->getTemporaryFile('input.pdf');
×
754
                        file_put_contents($input, $originalContent);
×
755

756
                        try {
757
                                $pdfContent = $this->pdf->applyStamp($input, $stamp);
×
758
                        } catch (RuntimeException $e) {
×
759
                                throw new LibresignException($e->getMessage());
×
760
                        }
761
                } else {
762
                        $pdfContent = $originalContent;
×
763
                }
764
                return $this->createSignedFile($originalFile, $pdfContent);
×
765
        }
766

767
        protected function getSignedFile(): ?File {
768
                $nodeId = $this->libreSignFile->getSignedNodeId();
3✔
769
                if (!$nodeId) {
3✔
770
                        return null;
1✔
771
                }
772

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

775
                if ($fileToSign->getOwner()->getUID() !== $this->libreSignFile->getUserId()) {
2✔
776
                        $fileToSign = $this->getNodeByIdUsingUid($fileToSign->getOwner()->getUID(), $nodeId);
1✔
777
                }
778
                return $fileToSign;
2✔
779
        }
780

781
        protected function getNodeByIdUsingUid(string $uid, int $nodeId): File {
782
                try {
783
                        $fileToSign = $this->root->getUserFolder($uid)->getFirstNodeById($nodeId);
4✔
784
                } catch (NoUserException) {
2✔
785
                        throw new LibresignException($this->l10n->t('User not found.'));
1✔
786
                } catch (NotPermittedException) {
1✔
787
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
788
                }
789
                if (!$fileToSign instanceof File) {
2✔
790
                        throw new LibresignException($this->l10n->t('File not found'));
1✔
791
                }
792
                return $fileToSign;
1✔
793
        }
794

795
        private function createSignedFile(File $originalFile, string $content): File {
796
                $filename = preg_replace(
×
797
                        '/' . $originalFile->getExtension() . '$/',
×
798
                        $this->l10n->t('signed') . '.' . $originalFile->getExtension(),
×
799
                        basename($originalFile->getPath())
×
800
                );
×
801
                $owner = $originalFile->getOwner()->getUID();
×
802
                try {
803
                        /** @var \OCP\Files\Folder */
804
                        $parentFolder = $this->root->getUserFolder($owner)->getFirstNodeById($originalFile->getParentId());
×
805
                        return $parentFolder->newFile($filename, $content);
×
806
                } catch (NotPermittedException) {
×
807
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
×
808
                }
809
        }
810

811
        /**
812
         * @throws DoesNotExistException
813
         */
814
        public function getSignRequestByUuid(string $uuid): SignRequestEntity {
815
                $this->validateHelper->validateUuidFormat($uuid);
4✔
816
                return $this->signRequestMapper->getByUuid($uuid);
3✔
817
        }
818

819
        /**
820
         * @throws DoesNotExistException
821
         */
822
        public function getFile(int $signRequestId): FileEntity {
823
                return $this->fileMapper->getById($signRequestId);
×
824
        }
825

826
        /**
827
         * @throws DoesNotExistException
828
         */
829
        public function getFileByUuid(string $uuid): FileEntity {
830
                return $this->fileMapper->getByUuid($uuid);
×
831
        }
832

833
        public function getIdDocById(int $fileId): IdDocs {
834
                return $this->idDocsMapper->getByFileId($fileId);
×
835
        }
836

837
        public function getNextcloudFile(FileEntity $fileData): File {
838
                $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($fileData->getNodeId());
1✔
839
                if (!$fileToSign instanceof File) {
1✔
840
                        throw new LibresignException(json_encode([
1✔
841
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
842
                                'errors' => [['message' => $this->l10n->t('File not found')]],
1✔
843
                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
1✔
844
                }
845
                return $fileToSign;
×
846
        }
847

848
        public function validateSigner(string $uuid, ?IUser $user = null): void {
849
                $this->validateHelper->validateSigner($uuid, $user);
×
850
        }
851

852
        public function validateRenewSigner(string $uuid, ?IUser $user = null): void {
853
                $this->validateHelper->validateRenewSigner($uuid, $user);
×
854
        }
855

856
        public function getSignerData(?IUser $user, ?SignRequestEntity $signRequest = null): array {
857
                $return = ['user' => ['name' => null]];
×
858
                if ($signRequest) {
×
859
                        $return['user']['name'] = $signRequest->getDisplayName();
×
860
                } elseif ($user) {
×
861
                        $return['user']['name'] = $user->getDisplayName();
×
862
                }
863
                return $return;
×
864
        }
865

866
        public function getAvailableIdentifyMethodsFromSettings(): array {
867
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsSettings();
×
868
                $return = array_map(fn (array $identifyMethod): array => [
×
869
                        'mandatory' => $identifyMethod['mandatory'],
×
870
                        'identifiedAtDate' => null,
×
871
                        'validateCode' => false,
×
872
                        'method' => $identifyMethod['name'],
×
873
                ], $identifyMethods);
×
874
                return $return;
×
875
        }
876

877
        /**
878
         * @psalm-return array{file?: File, nodeId?: int, url?: string, base64?: string}
879
         */
880
        public function getFileUrl(string $format, FileEntity $fileEntity, File $fileToSign, string $uuid): array {
881
                $url = [];
×
882
                switch ($format) {
883
                        case 'base64':
×
884
                                $url = ['base64' => base64_encode($fileToSign->getContent())];
×
885
                                break;
×
886
                        case 'url':
×
887
                                try {
888
                                        $this->idDocsMapper->getByFileId($fileEntity->getId());
×
889
                                        $url = ['url' => $this->urlGenerator->linkToRoute('libresign.page.getPdf', ['uuid' => $uuid])];
×
890
                                } catch (DoesNotExistException) {
×
891
                                        $url = ['url' => $this->urlGenerator->linkToRoute('libresign.page.getPdfFile', ['uuid' => $uuid])];
×
892
                                }
893
                                break;
×
894
                        case 'nodeId':
×
895
                                $url = ['nodeId' => $fileToSign->getId()];
×
896
                                break;
×
897
                        case 'file':
×
898
                                $url = ['file' => $fileToSign];
×
899
                                break;
×
900
                }
901
                return $url;
×
902
        }
903
}
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