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

LibreSign / libresign / 20600331344

30 Dec 2025 03:48PM UTC coverage: 45.001%. First build
20600331344

Pull #6262

github

web-flow
Merge 04f5c0a99 into 8a0afc934
Pull Request #6262: fix: envelope child handling

13 of 15 new or added lines in 3 files covered. (86.67%)

6608 of 14684 relevant lines covered (45.0%)

5.09 hits per line

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

55.95
/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 = null;
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
                private FileStatusService $fileStatusService,
110
        ) {
111
        }
139✔
112

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

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

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

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

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

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

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

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

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

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

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

228
                return $this;
5✔
229
        }
230

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

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

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

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

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

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

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

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

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

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

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

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

317
                return null;
3✔
318
        }
319

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

327
        public function sign(): void {
328
                $originalLibreSignFile = $this->libreSignFile;
18✔
329
                $originalSignRequest = $this->signRequest;
18✔
330
                $envelopeLastSignedDate = null;
18✔
331
                $lastSignedFile = null;
18✔
332

333
                $signRequests = $this->getSignRequestsToSign();
18✔
334

335
                if (empty($signRequests)) {
18✔
336
                        throw new LibresignException('No sign requests found to process');
×
337
                }
338

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

346
                        $this->validateDocMdpAllowsSignatures();
18✔
347
                        $signedFile = $this->getEngine()->sign();
16✔
348
                        $lastSignedFile = $signedFile;
16✔
349

350
                        $hash = $this->computeHash($signedFile);
16✔
351
                        $envelopeLastSignedDate = $this->getEngine()->getLastSignedDate();
16✔
352

353
                        $this->updateSignRequest($hash);
16✔
354
                        $this->updateLibreSignFile($signedFile->getId(), $hash);
16✔
355

356
                        $this->dispatchSignedEvent();
16✔
357
                }
358

359
                $this->libreSignFile = $originalLibreSignFile;
16✔
360
                $this->signRequest = $originalSignRequest;
16✔
361

362
                if ($originalLibreSignFile->isEnvelope()) {
16✔
363
                        if ($envelopeLastSignedDate) {
×
364
                                $this->signRequest->setSigned($envelopeLastSignedDate);
×
365
                                $this->signRequest->setStatusEnum(\OCA\Libresign\Enum\SignRequestStatus::SIGNED);
×
366
                                $this->signRequestMapper->update($this->signRequest);
×
367
                                $this->sequentialSigningService
×
368
                                        ->setFile($this->libreSignFile)
×
369
                                        ->releaseNextOrder(
×
370
                                                $this->signRequest->getFileId(),
×
371
                                                $this->signRequest->getSigningOrder()
×
372
                                        );
×
373
                        }
374
                        $this->updateEnvelopeStatus();
×
375

376
                        if ($lastSignedFile instanceof File) {
×
377
                                $event = $this->signedEventFactory->make(
×
378
                                        $this->signRequest,
×
379
                                        $this->libreSignFile,
×
380
                                        $lastSignedFile,
×
381
                                );
×
382
                                $this->eventDispatcher->dispatchTyped($event);
×
383
                        }
384
                }
385
        }
386

387
        /**
388
         * @return array Array of ['file' => FileEntity, 'signRequest' => SignRequestEntity]
389
         */
390
        private function getSignRequestsToSign(): array {
391
                if (!$this->libreSignFile->isEnvelope()
19✔
392
                        && !$this->libreSignFile->hasParent()
19✔
393
                ) {
394
                        return [[
18✔
395
                                'file' => $this->libreSignFile,
18✔
396
                                'signRequest' => $this->signRequest,
18✔
397
                        ]];
18✔
398
                }
399

400
                $envelopeId = $this->libreSignFile->isEnvelope()
1✔
NEW
401
                        ? $this->libreSignFile->getId()
×
402
                        : $this->libreSignFile->getParentFileId();
1✔
403

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

409
                $childSignRequests = $this->signRequestMapper->getByEnvelopeChildrenAndIdentifyMethod(
1✔
410
                        $envelopeId,
1✔
411
                        $this->signRequest->getId()
1✔
412
                );
1✔
413

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

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

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

433
                return $signRequestsData;
1✔
434
        }
435

436
        private function updateEnvelopeStatus(): void {
437
                $childFiles = $this->fileMapper->getChildrenFiles($this->libreSignFile->getId());
×
438

439
                $totalSignRequests = 0;
×
440
                $signedSignRequests = 0;
×
441

442
                foreach ($childFiles as $childFile) {
×
443
                        $signRequests = $this->signRequestMapper->getByFileId($childFile->getId());
×
444
                        $totalSignRequests += count($signRequests);
×
445

446
                        foreach ($signRequests as $signRequest) {
×
447
                                if ($signRequest->getSigned()) {
×
448
                                        $signedSignRequests++;
×
449
                                }
450
                        }
451
                }
452

453
                if ($totalSignRequests === 0) {
×
454
                        $this->libreSignFile->setStatus(FileEntity::STATUS_DRAFT);
×
455
                } elseif ($signedSignRequests === 0) {
×
456
                        $this->libreSignFile->setStatus(FileEntity::STATUS_ABLE_TO_SIGN);
×
457
                } elseif ($signedSignRequests === $totalSignRequests) {
×
458
                        $this->libreSignFile->setStatus(FileEntity::STATUS_SIGNED);
×
459
                } else {
460
                        $this->libreSignFile->setStatus(FileEntity::STATUS_PARTIAL_SIGNED);
×
461
                }
462

463
                $this->fileMapper->update($this->libreSignFile);
×
464
        }
465

466
        /**
467
         * @throws LibresignException If the document has DocMDP level 1 (no changes allowed)
468
         */
469
        protected function validateDocMdpAllowsSignatures(): void {
470
                $docmdpLevel = $this->libreSignFile->getDocmdpLevelEnum();
18✔
471

472
                if ($docmdpLevel === \OCA\Libresign\Enum\DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED) {
18✔
473
                        throw new LibresignException(
×
474
                                $this->l10n->t('This document has been certified with no changes allowed. You cannot add more signers to this document.'),
×
475
                                AppFrameworkHttp::STATUS_UNPROCESSABLE_ENTITY
×
476
                        );
×
477
                }
478

479
                if ($docmdpLevel === \OCA\Libresign\Enum\DocMdpLevel::NOT_CERTIFIED) {
18✔
480
                        $resource = $this->getLibreSignFileAsResource();
18✔
481

482
                        try {
483
                                if (!$this->docMdpHandler->allowsAdditionalSignatures($resource)) {
17✔
484
                                        throw new LibresignException(
3✔
485
                                                $this->l10n->t('This document has been certified with no changes allowed. You cannot add more signers to this document.'),
3✔
486
                                                AppFrameworkHttp::STATUS_UNPROCESSABLE_ENTITY
3✔
487
                                        );
3✔
488
                                }
489
                        } finally {
490
                                fclose($resource);
17✔
491
                        }
492
                }
493
        }
494

495
        /**
496
         * @return resource
497
         * @throws LibresignException
498
         */
499
        protected function getLibreSignFileAsResource() {
500
                $files = $this->getNextcloudFiles($this->libreSignFile);
12✔
501
                if (empty($files)) {
11✔
502
                        throw new LibresignException('File not found');
×
503
                }
504
                $fileToSign = current($files);
11✔
505
                $content = $fileToSign->getContent();
11✔
506
                $resource = fopen('php://memory', 'r+');
11✔
507
                if ($resource === false) {
11✔
508
                        throw new LibresignException('Failed to create temporary resource for PDF validation');
×
509
                }
510
                fwrite($resource, $content);
11✔
511
                rewind($resource);
11✔
512
                return $resource;
11✔
513
        }
514

515
        protected function computeHash(File $file): string {
516
                return hash('sha256', $file->getContent());
2✔
517
        }
518

519
        protected function updateSignRequest(string $hash): void {
520
                $lastSignedDate = $this->getEngine()->getLastSignedDate();
14✔
521
                $this->signRequest->setSigned($lastSignedDate);
14✔
522
                $this->signRequest->setSignedHash($hash);
14✔
523
                $this->signRequest->setStatusEnum(\OCA\Libresign\Enum\SignRequestStatus::SIGNED);
14✔
524

525
                $this->signRequestMapper->update($this->signRequest);
14✔
526

527
                $this->sequentialSigningService
14✔
528
                        ->setFile($this->libreSignFile)
14✔
529
                        ->releaseNextOrder(
14✔
530
                                $this->signRequest->getFileId(),
14✔
531
                                $this->signRequest->getSigningOrder()
14✔
532
                        );
14✔
533
        }
534

535
        protected function updateLibreSignFile(int $nodeId, string $hash): void {
536
                $this->libreSignFile->setSignedNodeId($nodeId);
14✔
537
                $this->libreSignFile->setSignedHash($hash);
14✔
538
                $this->setNewStatusIfNecessary();
14✔
539
                $this->fileMapper->update($this->libreSignFile);
14✔
540

541
                if ($this->libreSignFile->hasParent()) {
14✔
542
                        $this->fileStatusService->propagateStatusToParent($this->libreSignFile->getParentFileId());
×
543
                }
544
        }
545

546
        protected function dispatchSignedEvent(): void {
547
                $event = $this->signedEventFactory->make(
14✔
548
                        $this->signRequest,
14✔
549
                        $this->libreSignFile,
14✔
550
                        $this->getEngine()->getInputFile(),
14✔
551
                );
14✔
552
                $this->eventDispatcher->dispatchTyped($event);
14✔
553
        }
554

555
        protected function identifyEngine(File $file): SignEngineHandler {
556
                return $this->signEngineFactory->resolve($file->getExtension());
10✔
557
        }
558

559
        protected function getSignatureParams(): array {
560
                $certificateData = $this->readCertificate();
15✔
561
                $signatureParams = $this->buildBaseSignatureParams($certificateData);
15✔
562
                $signatureParams = $this->addEmailToSignatureParams($signatureParams, $certificateData);
15✔
563
                $signatureParams = $this->addMetadataToSignatureParams($signatureParams);
15✔
564
                return $signatureParams;
15✔
565
        }
566

567
        private function buildBaseSignatureParams(array $certificateData): array {
568
                return [
15✔
569
                        'DocumentUUID' => $this->libreSignFile?->getUuid(),
15✔
570
                        'IssuerCommonName' => $certificateData['issuer']['CN'] ?? '',
15✔
571
                        'SignerCommonName' => $certificateData['subject']['CN'] ?? '',
15✔
572
                        'LocalSignerTimezone' => $this->dateTimeZone->getTimeZone()->getName(),
15✔
573
                        'LocalSignerSignatureDateTime' => (new DateTime('now', new \DateTimeZone('UTC')))
15✔
574
                                ->format(DateTimeInterface::ATOM)
15✔
575
                ];
15✔
576
        }
577

578
        private function addEmailToSignatureParams(array $signatureParams, array $certificateData): array {
579
                if (isset($certificateData['extensions']['subjectAltName'])) {
15✔
580
                        preg_match('/(?:email:)+(?<email>[^\s,]+)/', $certificateData['extensions']['subjectAltName'], $matches);
6✔
581
                        if ($matches && filter_var($matches['email'], FILTER_VALIDATE_EMAIL)) {
6✔
582
                                $signatureParams['SignerEmail'] = $matches['email'];
4✔
583
                        } elseif (filter_var($certificateData['extensions']['subjectAltName'], FILTER_VALIDATE_EMAIL)) {
2✔
584
                                $signatureParams['SignerEmail'] = $certificateData['extensions']['subjectAltName'];
1✔
585
                        }
586
                }
587
                if (empty($signatureParams['SignerEmail']) && $this->user instanceof IUser) {
15✔
588
                        $signatureParams['SignerEmail'] = $this->user->getEMailAddress();
1✔
589
                }
590
                if (empty($signatureParams['SignerEmail']) && $this->signRequest instanceof SignRequestEntity) {
15✔
591
                        $identifyMethod = $this->identifyMethodService->getIdentifiedMethod($this->signRequest->getId());
9✔
592
                        if ($identifyMethod->getName() === IdentifyMethodService::IDENTIFY_EMAIL) {
9✔
593
                                $signatureParams['SignerEmail'] = $identifyMethod->getEntity()->getIdentifierValue();
1✔
594
                        }
595
                }
596
                return $signatureParams;
15✔
597
        }
598

599
        private function addMetadataToSignatureParams(array $signatureParams): array {
600
                $signRequestMetadata = $this->signRequest->getMetadata();
15✔
601
                if (isset($signRequestMetadata['remote-address'])) {
15✔
602
                        $signatureParams['SignerIP'] = $signRequestMetadata['remote-address'];
2✔
603
                }
604
                if (isset($signRequestMetadata['user-agent'])) {
15✔
605
                        $signatureParams['SignerUserAgent'] = $signRequestMetadata['user-agent'];
2✔
606
                }
607
                return $signatureParams;
15✔
608
        }
609

610
        public function storeUserMetadata(array $metadata = []): self {
611
                $collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
18✔
612
                if (!$collectMetadata || !$metadata) {
18✔
613
                        return $this;
7✔
614
                }
615
                $this->signRequest->setMetadata(array_merge(
11✔
616
                        $this->signRequest->getMetadata() ?? [],
11✔
617
                        $metadata,
11✔
618
                ));
11✔
619
                $this->signRequestMapper->update($this->signRequest);
11✔
620
                return $this;
11✔
621
        }
622

623
        /**
624
         * @return SignRequestEntity[]
625
         */
626
        protected function getSigners(): array {
627
                if (empty($this->signers)) {
×
628
                        $this->signers = $this->signRequestMapper->getByFileId($this->signRequest->getFileId());
×
629
                        if ($this->signers) {
×
630
                                foreach ($this->signers as $key => $signer) {
×
631
                                        if ($signer->getId() === $this->signRequest->getId()) {
×
632
                                                $this->signers[$key] = $this->signRequest;
×
633
                                                break;
×
634
                                        }
635
                                }
636
                        }
637
                }
638
                return $this->signers;
×
639
        }
640

641
        protected function setNewStatusIfNecessary(): bool {
642
                $newStatus = $this->evaluateStatusFromSigners();
10✔
643

644
                if ($newStatus === null || $newStatus === $this->libreSignFile->getStatus()) {
10✔
645
                        return false;
4✔
646
                }
647

648
                $this->libreSignFile->setStatus($newStatus);
6✔
649
                return true;
6✔
650
        }
651

652
        private function evaluateStatusFromSigners(): ?int {
653
                $signers = $this->getSigners();
10✔
654

655
                $total = count($signers);
10✔
656

657
                if ($total === 0) {
10✔
658
                        return null;
1✔
659
                }
660

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

663
                if ($totalSigned === $total) {
9✔
664
                        return FileEntity::STATUS_SIGNED;
5✔
665
                }
666

667
                if ($totalSigned > 0) {
4✔
668
                        return FileEntity::STATUS_PARTIAL_SIGNED;
3✔
669
                }
670

671
                return null;
1✔
672
        }
673

674
        private function getOrGeneratePfxContent(SignEngineHandler $engine): string {
675
                if ($certificate = $engine->getCertificate()) {
12✔
676
                        return $certificate;
×
677
                }
678
                if ($this->signWithoutPassword) {
12✔
679
                        $tempPassword = $this->generateTemporaryPassword();
1✔
680
                        $this->setPassword($tempPassword);
1✔
681
                        $engine->generateCertificate(
1✔
682
                                [
1✔
683
                                        'host' => $this->userUniqueIdentifier,
1✔
684
                                        'uid' => $this->userUniqueIdentifier,
1✔
685
                                        'name' => $this->friendlyName,
1✔
686
                                ],
1✔
687
                                $tempPassword,
1✔
688
                                $this->friendlyName,
1✔
689
                        );
1✔
690
                }
691
                return $engine->getPfxOfCurrentSigner();
12✔
692
        }
693

694
        private function generateTemporaryPassword(): string {
695
                $passwordEvent = new GenerateSecurePasswordEvent();
1✔
696
                $this->eventDispatcher->dispatchTyped($passwordEvent);
1✔
697
                return $passwordEvent->getPassword() ?? $this->secureRandom->generate(20);
1✔
698
        }
699

700
        protected function readCertificate(): array {
701
                return $this->getEngine()
×
702
                        ->readCertificate();
×
703
        }
704

705
        /**
706
         * Get file to sign
707
         *
708
         * @throws LibresignException
709
         */
710
        protected function getFileToSign(): File {
711
                if ($this->fileToSign instanceof File) {
×
712
                        return $this->fileToSign;
×
713
                }
714

715
                $userId = $this->libreSignFile->getUserId();
×
716
                $nodeId = $this->libreSignFile->getNodeId();
×
717

718
                $originalFile = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
×
719
                if (!$originalFile instanceof File) {
×
720
                        throw new LibresignException($this->l10n->t('File not found'));
×
721
                }
722
                if ($this->isPdf($originalFile)) {
×
723
                        $this->fileToSign = $this->getPdfToSign($originalFile);
×
724
                } else {
725
                        $this->fileToSign = $originalFile;
×
726
                }
727
                return $this->fileToSign;
×
728
        }
729

730
        private function isPdf(File $file): bool {
731
                return strcasecmp($file->getExtension(), 'pdf') === 0;
×
732
        }
733

734
        protected function getEngine(): SignEngineHandler {
735
                if (!$this->engine) {
12✔
736
                        $originalFile = $this->getFileToSign();
12✔
737
                        $this->engine = $this->identifyEngine($originalFile);
12✔
738

739
                        $this->configureEngine();
12✔
740
                }
741
                return $this->engine;
12✔
742
        }
743

744
        private function configureEngine(): void {
745
                $this->engine
12✔
746
                        ->setInputFile($this->getFileToSign())
12✔
747
                        ->setCertificate($this->getOrGeneratePfxContent($this->engine))
12✔
748
                        ->setPassword($this->password);
12✔
749

750
                if ($this->engine::class === Pkcs12Handler::class) {
12✔
751
                        $this->engine
2✔
752
                                ->setVisibleElements($this->getVisibleElements())
2✔
753
                                ->setSignatureParams($this->getSignatureParams());
2✔
754
                }
755
        }
756

757
        public function getLibresignFile(?int $nodeId, ?string $signRequestUuid = null): FileEntity {
758
                try {
759
                        if ($nodeId) {
3✔
760
                                return $this->fileMapper->getByNodeId($nodeId);
1✔
761
                        }
762

763
                        if ($signRequestUuid) {
2✔
764
                                $signRequest = $this->signRequestMapper->getByUuid($signRequestUuid);
2✔
765
                                return $this->fileMapper->getById($signRequest->getFileId());
2✔
766
                        }
767

768
                        throw new \Exception('Invalid arguments');
×
769

770
                } catch (DoesNotExistException) {
1✔
771
                        throw new LibresignException($this->l10n->t('File not found'), 1);
1✔
772
                }
773
        }
774

775
        public function renew(SignRequestEntity $signRequest, string $method): void {
776
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
777
                if (empty($identifyMethods[$method])) {
×
778
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
×
779
                }
780

781
                $signRequest->setUuid(UUIDUtil::getUUID());
×
782
                $this->signRequestMapper->update($signRequest);
×
783

784
                array_map(function (IIdentifyMethod $identifyMethod): void {
×
785
                        $entity = $identifyMethod->getEntity();
×
786
                        $entity->setAttempts($entity->getAttempts() + 1);
×
787
                        $entity->setLastAttemptDate($this->timeFactory->getDateTime());
×
788
                        $identifyMethod->save();
×
789
                }, $identifyMethods[$method]);
×
790
        }
791

792
        public function requestCode(
793
                SignRequestEntity $signRequest,
794
                string $identifyMethodName,
795
                string $signMethodName,
796
                string $identify = '',
797
        ): void {
798
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
799
                if (empty($identifyMethods[$identifyMethodName])) {
×
800
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
×
801
                }
802
                foreach ($identifyMethods[$identifyMethodName] as $identifyMethod) {
×
803
                        try {
804
                                $signatureMethod = $identifyMethod->getEmptyInstanceOfSignatureMethodByName($signMethodName);
×
805
                                $signatureMethod->setEntity($identifyMethod->getEntity());
×
806
                        } catch (InvalidArgumentException) {
×
807
                                continue;
×
808
                        }
809
                        /** @var IToken $signatureMethod */
810
                        $identifier = $identify ?: $identifyMethod->getEntity()->getIdentifierValue();
×
811
                        $signatureMethod->requestCode($identifier, $identifyMethod->getEntity()->getIdentifierKey());
×
812
                        return;
×
813
                }
814
                throw new LibresignException($this->l10n->t('Sending authorization code not enabled.'));
×
815
        }
816

817
        public function getSignRequestToSign(FileEntity $libresignFile, ?string $signRequestUuid, ?IUser $user): SignRequestEntity {
818
                $this->validateHelper->fileCanBeSigned($libresignFile);
2✔
819
                try {
820
                        if ($libresignFile->isEnvelope()) {
2✔
821
                                $childFiles = $this->fileMapper->getChildrenFiles($libresignFile->getId());
×
822
                                $allSignRequests = [];
×
823
                                foreach ($childFiles as $childFile) {
×
824
                                        $childSignRequests = $this->signRequestMapper->getByFileId($childFile->getId());
×
825
                                        $allSignRequests = array_merge($allSignRequests, $childSignRequests);
×
826
                                }
827
                                $signRequests = $allSignRequests;
×
828
                        } else {
829
                                $signRequests = $this->signRequestMapper->getByFileId($libresignFile->getId());
2✔
830
                        }
831

832
                        if (!empty($signRequestUuid)) {
2✔
833
                                $signRequest = $this->getSignRequestByUuid($signRequestUuid);
2✔
834
                        } else {
835
                                $signRequest = array_reduce($signRequests, function (?SignRequestEntity $carry, SignRequestEntity $signRequest) use ($user): ?SignRequestEntity {
×
836
                                        $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
837
                                        $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($user) {
×
838
                                                if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL
×
839
                                                        && $user
840
                                                        && (
841
                                                                $identifyMethod->getIdentifierValue() === $user->getUID()
×
842
                                                                || $identifyMethod->getIdentifierValue() === $user->getEMailAddress()
×
843
                                                        )
844
                                                ) {
845
                                                        return true;
×
846
                                                }
847
                                                if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT
×
848
                                                        && $user
849
                                                        && $identifyMethod->getIdentifierValue() === $user->getUID()
×
850
                                                ) {
851
                                                        return true;
×
852
                                                }
853
                                                return false;
×
854
                                        });
×
855
                                        if (count($found) > 0) {
×
856
                                                return $signRequest;
×
857
                                        }
858
                                        return $carry;
×
859
                                });
×
860
                        }
861

862
                        if (!$signRequest) {
2✔
863
                                throw new DoesNotExistException('Sign request not found');
×
864
                        }
865
                        if ($signRequest->getSigned()) {
2✔
866
                                throw new LibresignException($this->l10n->t('File already signed by you'), 1);
×
867
                        }
868
                        return $signRequest;
2✔
869
                } catch (DoesNotExistException) {
×
870
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
871
                }
872
        }
873

874
        protected function getPdfToSign(File $originalFile): File {
875
                $file = $this->getSignedFile();
×
876
                if ($file instanceof File) {
×
877
                        return $file;
×
878
                }
879

880
                $originalContent = $originalFile->getContent();
×
881

882
                if ($this->pdfSignatureDetectionService->hasSignatures($originalContent)) {
×
883
                        return $this->createSignedFile($originalFile, $originalContent);
×
884
                }
885
                $metadata = $this->footerHandler->getMetadata($originalFile, $this->libreSignFile);
×
886
                $footer = $this->footerHandler
×
887
                        ->setTemplateVar('uuid', $this->libreSignFile->getUuid())
×
888
                        ->setTemplateVar('signers', array_map(fn (SignRequestEntity $signer) => [
×
889
                                'displayName' => $signer->getDisplayName(),
×
890
                                'signed' => $signer->getSigned()
×
891
                                        ? $signer->getSigned()->format(DateTimeInterface::ATOM)
×
892
                                        : null,
893
                        ], $this->getSigners()))
×
894
                        ->getFooter($metadata['d']);
×
895
                if ($footer) {
×
896
                        $stamp = $this->tempManager->getTemporaryFile('stamp.pdf');
×
897
                        file_put_contents($stamp, $footer);
×
898

899
                        $input = $this->tempManager->getTemporaryFile('input.pdf');
×
900
                        file_put_contents($input, $originalContent);
×
901

902
                        try {
903
                                $pdfContent = $this->pdf->applyStamp($input, $stamp);
×
904
                        } catch (RuntimeException $e) {
×
905
                                throw new LibresignException($e->getMessage());
×
906
                        }
907
                } else {
908
                        $pdfContent = $originalContent;
×
909
                }
910
                return $this->createSignedFile($originalFile, $pdfContent);
×
911
        }
912

913
        protected function getSignedFile(): ?File {
914
                $nodeId = $this->libreSignFile->getSignedNodeId();
3✔
915
                if (!$nodeId) {
3✔
916
                        return null;
1✔
917
                }
918

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

921
                if ($fileToSign->getOwner()->getUID() !== $this->libreSignFile->getUserId()) {
2✔
922
                        $fileToSign = $this->getNodeByIdUsingUid($fileToSign->getOwner()->getUID(), $nodeId);
1✔
923
                }
924
                return $fileToSign;
2✔
925
        }
926

927
        protected function getNodeByIdUsingUid(string $uid, int $nodeId): File {
928
                try {
929
                        $fileToSign = $this->root->getUserFolder($uid)->getFirstNodeById($nodeId);
4✔
930
                } catch (NoUserException) {
2✔
931
                        throw new LibresignException($this->l10n->t('User not found.'));
1✔
932
                } catch (NotPermittedException) {
1✔
933
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
934
                }
935
                if (!$fileToSign instanceof File) {
2✔
936
                        throw new LibresignException($this->l10n->t('File not found'));
1✔
937
                }
938
                return $fileToSign;
1✔
939
        }
940

941
        private function createSignedFile(File $originalFile, string $content): File {
942
                $filename = preg_replace(
×
943
                        '/' . $originalFile->getExtension() . '$/',
×
944
                        $this->l10n->t('signed') . '.' . $originalFile->getExtension(),
×
945
                        basename($originalFile->getPath())
×
946
                );
×
947
                $owner = $originalFile->getOwner()->getUID();
×
948
                try {
949
                        /** @var \OCP\Files\Folder */
950
                        $parentFolder = $this->root->getUserFolder($owner)->getFirstNodeById($originalFile->getParentId());
×
951
                        return $parentFolder->newFile($filename, $content);
×
952
                } catch (NotPermittedException) {
×
953
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
×
954
                }
955
        }
956

957
        /**
958
         * @throws DoesNotExistException
959
         */
960
        public function getSignRequestByUuid(string $uuid): SignRequestEntity {
961
                $this->validateHelper->validateUuidFormat($uuid);
4✔
962
                return $this->signRequestMapper->getByUuid($uuid);
3✔
963
        }
964

965
        /**
966
         * @throws DoesNotExistException
967
         */
968
        public function getFile(int $signRequestId): FileEntity {
969
                return $this->fileMapper->getById($signRequestId);
×
970
        }
971

972
        /**
973
         * @throws DoesNotExistException
974
         */
975
        public function getFileByUuid(string $uuid): FileEntity {
976
                return $this->fileMapper->getByUuid($uuid);
×
977
        }
978

979
        public function getIdDocById(int $fileId): IdDocs {
980
                return $this->idDocsMapper->getByFileId($fileId);
×
981
        }
982

983
        /**
984
         * @return File[] Array of files
985
         */
986
        public function getNextcloudFiles(FileEntity $fileData): array {
987
                if ($fileData->getNodeType() === 'envelope') {
1✔
988
                        $children = $this->fileMapper->getChildrenFiles($fileData->getId());
×
989
                        $files = [];
×
990
                        foreach ($children as $child) {
×
991
                                $file = $this->root->getUserFolder($child->getUserId())->getFirstNodeById($child->getNodeId());
×
992
                                if ($file instanceof File) {
×
993
                                        $files[] = $file;
×
994
                                }
995
                        }
996
                        return $files;
×
997
                }
998

999
                $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($fileData->getNodeId());
1✔
1000
                if (!$fileToSign instanceof File) {
1✔
1001
                        throw new LibresignException(json_encode([
1✔
1002
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
1003
                                'errors' => [['message' => $this->l10n->t('File not found')]],
1✔
1004
                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
1✔
1005
                }
1006
                return [$fileToSign];
×
1007
        }
1008

1009
        /**
1010
         * @return array<FileEntity>
1011
         */
1012
        public function getNextcloudFilesWithEntities(FileEntity $fileData): array {
1013
                if ($fileData->getNodeType() === 'envelope') {
×
1014
                        $children = $this->fileMapper->getChildrenFiles($fileData->getId());
×
1015
                        $result = [];
×
1016
                        foreach ($children as $child) {
×
1017
                                $file = $this->root->getUserFolder($child->getUserId())->getFirstNodeById($child->getNodeId());
×
1018
                                if ($file instanceof File) {
×
1019
                                        $result[] = $child;
×
1020
                                }
1021
                        }
1022
                        return $result;
×
1023
                }
1024

1025
                $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($fileData->getNodeId());
×
1026
                if (!$fileToSign instanceof File) {
×
1027
                        throw new LibresignException(json_encode([
×
1028
                                'action' => JSActions::ACTION_DO_NOTHING,
×
1029
                                'errors' => [['message' => $this->l10n->t('File not found')]],
×
1030
                        ]), AppFrameworkHttp::STATUS_NOT_FOUND);
×
1031
                }
1032
                return [$fileData];
×
1033
        }
1034

1035
        public function validateSigner(string $uuid, ?IUser $user = null): void {
1036
                $this->validateHelper->validateSigner($uuid, $user);
×
1037
        }
1038

1039
        public function validateRenewSigner(string $uuid, ?IUser $user = null): void {
1040
                $this->validateHelper->validateRenewSigner($uuid, $user);
×
1041
        }
1042

1043
        public function getSignerData(?IUser $user, ?SignRequestEntity $signRequest = null): array {
1044
                $return = ['user' => ['name' => null]];
×
1045
                if ($signRequest) {
×
1046
                        $return['user']['name'] = $signRequest->getDisplayName();
×
1047
                } elseif ($user) {
×
1048
                        $return['user']['name'] = $user->getDisplayName();
×
1049
                }
1050
                return $return;
×
1051
        }
1052

1053
        public function getAvailableIdentifyMethodsFromSettings(): array {
1054
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsSettings();
×
1055
                $return = array_map(fn (array $identifyMethod): array => [
×
1056
                        'mandatory' => $identifyMethod['mandatory'],
×
1057
                        'identifiedAtDate' => null,
×
1058
                        'validateCode' => false,
×
1059
                        'method' => $identifyMethod['name'],
×
1060
                ], $identifyMethods);
×
1061
                return $return;
×
1062
        }
1063

1064
        public function getFileUrl(int $fileId, string $uuid): string {
1065
                try {
1066
                        $this->idDocsMapper->getByFileId($fileId);
×
1067
                        return $this->urlGenerator->linkToRoute('libresign.page.getPdf', ['uuid' => $uuid]);
×
1068
                } catch (DoesNotExistException) {
×
1069
                        return $this->urlGenerator->linkToRoute('libresign.page.getPdfFile', ['uuid' => $uuid]);
×
1070
                }
1071
        }
1072

1073
        /**
1074
         * Get PDF URLs for signing
1075
         * For envelopes: returns URLs for all child files
1076
         * For regular files: returns URL for the file itself
1077
         *
1078
         * @return string[]
1079
         */
1080
        public function getPdfUrlsForSigning(FileEntity $fileEntity, SignRequestEntity $signRequestEntity): array {
1081
                if (!$fileEntity->isEnvelope()) {
×
1082
                        return [
×
1083
                                $this->getFileUrl($fileEntity->getId(), $signRequestEntity->getUuid())
×
1084
                        ];
×
1085
                }
1086

1087
                $childSignRequests = $this->signRequestMapper->getByEnvelopeChildrenAndIdentifyMethod(
×
1088
                        $fileEntity->getId(),
×
1089
                        $signRequestEntity->getId()
×
1090
                );
×
1091

1092
                $pdfUrls = [];
×
1093
                foreach ($childSignRequests as $childSignRequest) {
×
1094
                        $pdfUrls[] = $this->getFileUrl(
×
1095
                                $childSignRequest->getFileId(),
×
1096
                                $childSignRequest->getUuid()
×
1097
                        );
×
1098
                }
1099

1100
                return $pdfUrls;
×
1101
        }
1102
}
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