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

LibreSign / libresign / 4666181903

pending completion
4666181903

push

github

GitHub
Merge pull request #1608 from LibreSign/backport/1584/stable25

10 of 15 new or added lines in 4 files covered. (66.67%)

1 existing line in 1 file now uncovered.

2532 of 4431 relevant lines covered (57.14%)

4.63 hits per line

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

71.95
/lib/Service/SignFileService.php
1
<?php
2

3
namespace OCA\Libresign\Service;
4

5
use OCA\Libresign\AppInfo\Application;
6
use OCA\Libresign\DataObjects\VisibleElementAssoc;
7
use OCA\Libresign\Db\AccountFileMapper;
8
use OCA\Libresign\Db\File as FileEntity;
9
use OCA\Libresign\Db\FileElementMapper;
10
use OCA\Libresign\Db\FileMapper;
11
use OCA\Libresign\Db\FileUser as FileUserEntity;
12
use OCA\Libresign\Db\FileUserMapper;
13
use OCA\Libresign\Db\UserElementMapper;
14
use OCA\Libresign\Event\SignedEvent;
15
use OCA\Libresign\Exception\LibresignException;
16
use OCA\Libresign\Handler\Pkcs7Handler;
17
use OCA\Libresign\Handler\Pkcs12Handler;
18
use OCA\Libresign\Helper\JSActions;
19
use OCA\Libresign\Helper\ValidateHelper;
20
use OCP\Accounts\IAccountManager;
21
use OCP\App\IAppManager;
22
use OCP\AppFramework\Db\DoesNotExistException;
23
use OCP\AppFramework\Http;
24
use OCP\AppFramework\OCS\OCSForbiddenException;
25
use OCP\EventDispatcher\IEventDispatcher;
26
use OCP\Files\File;
27
use OCP\Files\IMimeTypeDetector;
28
use OCP\Files\IRootFolder;
29
use OCP\Http\Client\IClientService;
30
use OCP\IConfig;
31
use OCP\IL10N;
32
use OCP\ITempManager;
33
use OCP\IURLGenerator;
34
use OCP\IUser;
35
use OCP\IUserManager;
36
use OCP\Security\IHasher;
37
use OCP\Security\ISecureRandom;
38
use Psr\Container\ContainerInterface;
39
use Psr\Log\LoggerInterface;
40
use Sabre\DAV\UUIDUtil;
41

42
class SignFileService {
43
        use TFile;
44

45
        /** @var IL10N */
46
        private $l10n;
47
        /** @var FileMapper */
48
        private $fileMapper;
49
        /** @var FileUserMapper */
50
        private $fileUserMapper;
51
        /** @var AccountFileMapper */
52
        private $accountFileMapper;
53
        /** @var Pkcs7Handler */
54
        private $pkcs7Handler;
55
        /** @var Pkcs12Handler */
56
        private $pkcs12Handler;
57
        /** @var FolderService */
58
        private $folderService;
59
        /** @var IClientService */
60
        private $client;
61
        /** @var IUserManager */
62
        private $userManager;
63
        /** @var MailService */
64
        private $mail;
65
        /** @var LoggerInterface */
66
        private $logger;
67
        /** @var IConfig */
68
        private $config;
69
        /** @var ValidateHelper */
70
        private $validateHelper;
71
        /** @var IHasher */
72
        private $hasher;
73
        /** @var ISecureRandom */
74
        private $secureRandom;
75
        /** @var IAppManager */
76
        private $appManager;
77
        /** @var IAccountManager */
78
        private $accountManager;
79
        /** @var ContainerInterface */
80
        private $serverContainer;
81
        /** @var IRootFolder */
82
        private $root;
83
        /** @var FileElementMapper */
84
        private $fileElementMapper;
85
        /** @var UserElementMapper */
86
        private $userElementMapper;
87
        /** @var FileElementService */
88
        private $fileElementService;
89
        /** @var IEventDispatcher */
90
        private $eventDispatcher;
91
        /** @var IURLGenerator */
92
        private $urlGenerator;
93
        /** @var IMimeTypeDetector */
94
        private $mimeTypeDetector;
95
        /** @var PdfParserService */
96
        private $pdfParserService;
97
        /** @var ITempManager */
98
        private $tempManager;
99
        /** @var FileUserEntity */
100
        private $fileUser;
101
        /** @var string */
102
        private $password;
103
        /** @var FileEntity */
104
        private $libreSignFile;
105
        /** @var VisibleElementAssoc[] */
106
        private $elements = [];
107
        /** @var bool */
108
        private $signWithoutPassword = false;
109

110
        public function __construct(
111
                IL10N $l10n,
112
                FileMapper $fileMapper,
113
                FileUserMapper $fileUserMapper,
114
                AccountFileMapper $accountFileMapper,
115
                Pkcs7Handler $pkcs7Handler,
116
                Pkcs12Handler $pkcs12Handler,
117
                FolderService $folderService,
118
                IClientService $client,
119
                IUserManager $userManager,
120
                MailService $mail,
121
                LoggerInterface $logger,
122
                IConfig $config,
123
                ValidateHelper $validateHelper,
124
                IHasher $hasher,
125
                ISecureRandom $secureRandom,
126
                IAppManager $appManager,
127
                IAccountManager $accountManager,
128
                ContainerInterface $serverContainer,
129
                IRootFolder $root,
130
                FileElementMapper $fileElementMapper,
131
                UserElementMapper $userElementMapper,
132
                FileElementService $fileElementService,
133
                IEventDispatcher $eventDispatcher,
134
                IURLGenerator $urlGenerator,
135
                PdfParserService $pdfParserService,
136
                IMimeTypeDetector $mimeTypeDetector,
137
                ITempManager $tempManager
138
        ) {
139
                $this->l10n = $l10n;
72✔
140
                $this->fileMapper = $fileMapper;
72✔
141
                $this->fileUserMapper = $fileUserMapper;
72✔
142
                $this->accountFileMapper = $accountFileMapper;
72✔
143
                $this->pkcs7Handler = $pkcs7Handler;
72✔
144
                $this->pkcs12Handler = $pkcs12Handler;
72✔
145
                $this->folderService = $folderService;
72✔
146
                $this->client = $client;
72✔
147
                $this->userManager = $userManager;
72✔
148
                $this->mail = $mail;
72✔
149
                $this->logger = $logger;
72✔
150
                $this->config = $config;
72✔
151
                $this->validateHelper = $validateHelper;
72✔
152
                $this->hasher = $hasher;
72✔
153
                $this->secureRandom = $secureRandom;
72✔
154
                $this->appManager = $appManager;
72✔
155
                $this->accountManager = $accountManager;
72✔
156
                $this->serverContainer = $serverContainer;
72✔
157
                $this->root = $root;
72✔
158
                $this->fileElementMapper = $fileElementMapper;
72✔
159
                $this->userElementMapper = $userElementMapper;
72✔
160
                $this->fileElementService = $fileElementService;
72✔
161
                $this->eventDispatcher = $eventDispatcher;
72✔
162
                $this->urlGenerator = $urlGenerator;
72✔
163
                $this->pdfParserService = $pdfParserService;
72✔
164
                $this->mimeTypeDetector = $mimeTypeDetector;
72✔
165
                $this->tempManager = $tempManager;
72✔
166
        }
167

168
        /**
169
         * @param array $data
170
         */
171
        public function save(array $data): array {
172
                $file = $this->saveFile($data);
17✔
173
                $this->saveVisibleElements($data, $file);
17✔
174
                $return['uuid'] = $file->getUuid();
17✔
175
                $return['nodeId'] = $file->getNodeId();
17✔
176
                $return['users'] = $this->associateToUsers($data, $file->getId());
17✔
177
                return $return;
17✔
178
        }
179

180
        private function saveVisibleElements(array $data, FileEntity $file): array {
181
                if (empty($data['visibleElements'])) {
20✔
182
                        return [];
18✔
183
                }
184
                $elements = $data['visibleElements'];
2✔
185
                foreach ($elements as $key => $element) {
2✔
186
                        $element['fileId'] = $file->getId();
2✔
187
                        $elements[$key] = $this->fileElementService->saveVisibleElement($element);
2✔
188
                }
189
                return $elements;
2✔
190
        }
191

192
        /**
193
         * Save file data
194
         *
195
         *
196
         * @param array{userManager: IUser, name: string, callback: string} $data
197
         */
198
        public function saveFile(array $data): FileEntity {
199
                if (!empty($data['uuid'])) {
18✔
200
                        return $this->fileMapper->getByUuid($data['uuid']);
3✔
201
                }
202
                if (!empty($data['file']['fileId'])) {
17✔
203
                        try {
204
                                $file = $this->fileMapper->getByFileId($data['file']['fileId']);
×
205
                                if (!empty($data['status']) && $data['status'] > $file->getStatus()) {
×
206
                                        $file->setStatus($data['status']);
×
207
                                        return $this->fileMapper->update($file);
×
208
                                }
209
                                return $file;
×
210
                        } catch (\Throwable $th) {
×
211
                        }
212
                }
213

214
                $node = $this->getNodeFromData($data);
17✔
215

216
                $file = new FileEntity();
17✔
217
                $file->setNodeId($node->getId());
17✔
218
                $file->setUserId($data['userManager']->getUID());
17✔
219
                $file->setUuid(UUIDUtil::getUUID());
17✔
220
                $file->setCreatedAt(time());
17✔
221
                $file->setName($data['name']);
17✔
222
                $file->setMetadata(json_encode($this->getFileMetadata($node)));
17✔
223
                if (!empty($data['callback'])) {
17✔
224
                        $file->setCallback($data['callback']);
×
225
                }
226
                if (isset($data['status'])) {
17✔
227
                        $file->setStatus($data['status']);
1✔
228
                } else {
229
                        $file->setStatus(FileEntity::STATUS_ABLE_TO_SIGN);
16✔
230
                }
231
                $this->fileMapper->insert($file);
17✔
232
                return $file;
17✔
233
        }
234

235
        public function getFileMetadata(\OCP\Files\Node $node): array {
236
                $metadata = [
17✔
237
                        'extension' => $node->getExtension(),
17✔
238
                ];
17✔
239
                if ($metadata['extension'] === 'pdf') {
17✔
240
                        $metadata = $this->pdfParserService->getMetadata($node);
17✔
241
                }
242
                return $metadata;
17✔
243
        }
244

245
        public function saveFileUser(FileUserEntity $fileUser, bool $notifyAsNewUser = false): void {
246
                if ($fileUser->getId()) {
19✔
247
                        $this->fileUserMapper->update($fileUser);
2✔
248
                } else {
249
                        $this->fileUserMapper->insert($fileUser);
17✔
250
                        $notifyAsNewUser = true;
17✔
251
                }
252
                if ($notifyAsNewUser) {
19✔
253
                        $this->mail->notifyUnsignedUser($fileUser);
17✔
254
                } else {
255
                        $this->mail->notifySignDataUpdated($fileUser);
2✔
256
                }
257
        }
258

259
        /**
260
         * @return FileUserEntity[]
261
         *
262
         * @psalm-return list<FileUserEntity>
263
         */
264
        private function associateToUsers(array $data, int $fileId): array {
265
                $return = [];
17✔
266
                if (!empty($data['users'])) {
17✔
267
                        $notifyAsNewUser = false;
17✔
268
                        if (isset($data['status']) && $data['status'] === FileEntity::STATUS_ABLE_TO_SIGN) {
17✔
269
                                $notifyAsNewUser = true;
1✔
270
                        }
271
                        foreach ($data['users'] as $user) {
17✔
272
                                $user['email'] = $this->getUserEmail($user);
17✔
273
                                $fileUser = $this->getFileUser($user['email'], $fileId);
17✔
274
                                $this->setDataToUser($fileUser, $user, $fileId);
17✔
275
                                $this->saveFileUser($fileUser, $notifyAsNewUser);
17✔
276
                                $return[] = $fileUser;
17✔
277
                        }
278
                }
279
                return $return;
17✔
280
        }
281

282
        public function getUserIdentifyMethod(array $user): string {
283
                if (array_key_exists('identify', $user)) {
21✔
NEW
284
                        return $user['identify'];
×
285
                }
286
                return $this->config->getAppValue(Application::APP_ID, 'identify_method', 'nextcloud') ?? 'nextcloud';
21✔
287
        }
288

289
        /**
290
         * @psalm-suppress MixedReturnStatement
291
         */
292
        private function getFileUser(string $email, int $fileId): FileUserEntity {
293
                try {
294
                        $fileUser = $this->fileUserMapper->getByEmailAndFileId($email, $fileId);
17✔
295
                } catch (DoesNotExistException $e) {
16✔
296
                        $fileUser = new FileUserEntity();
16✔
297
                }
298
                return $fileUser;
17✔
299
        }
300

301
        /**
302
         * @psalm-suppress MixedMethodCall
303
         */
304
        private function setDataToUser(FileUserEntity $fileUser, array $user, int $fileId): void {
305
                $fileUser->setFileId($fileId);
17✔
306
                if (!$fileUser->getUuid()) {
17✔
307
                        $fileUser->setUuid(UUIDUtil::getUUID());
17✔
308
                }
309
                $identifyMethod = $this->getUserIdentifyMethod($user);
17✔
310
                $fileUser->setIdentifyMethod($identifyMethod);
17✔
311
                $fileUser->setEmail($user['email']);
17✔
312
                if (!empty($user['description']) && $fileUser->getDescription() !== $user['description']) {
17✔
313
                        $fileUser->setDescription($user['description']);
1✔
314
                }
315
                if (empty($user['uid'])) {
17✔
316
                        $userToSign = $this->userManager->getByEmail($user['email']);
17✔
317
                        if ($userToSign) {
17✔
318
                                $fileUser->setUserId($userToSign[0]->getUID());
11✔
319
                                if (empty($user['displayName'])) {
11✔
320
                                        $user['displayName'] = $userToSign[0]->getDisplayName();
17✔
321
                                }
322
                        }
323
                } else {
324
                        $fileUser->setUserId($user['uid']);
×
325
                }
326
                if (!empty($user['displayName'])) {
17✔
327
                        $fileUser->setDisplayName($user['displayName']);
11✔
328
                }
329
                if (!$fileUser->getId()) {
17✔
330
                        $fileUser->setCreatedAt(time());
16✔
331
                }
332
        }
333

334
        public function validateNewRequestToFile(array $data): void {
335
                $this->validateUserManager($data);
9✔
336
                $this->validateNewFile($data);
8✔
337
                $this->validateUsers($data);
7✔
338
                $this->validateHelper->validateFileStatus($data);
2✔
339
        }
340

341
        public function validateUserManager(array $user): void {
342
                if (!isset($user['userManager'])) {
19✔
343
                        throw new \Exception($this->l10n->t('You are not allowed to request signing'), Http::STATUS_UNPROCESSABLE_ENTITY);
1✔
344
                }
345
                $this->validateHelper->canRequestSign($user['userManager']);
18✔
346
        }
347

348
        public function validateNewFile(array $data): void {
349
                if (empty($data['name'])) {
8✔
350
                        throw new \Exception($this->l10n->t('Name is mandatory'));
1✔
351
                }
352
                $this->validateHelper->validateNewFile($data);
7✔
353
        }
354

355
        public function validateExistingFile(array $data): void {
356
                if (isset($data['uuid'])) {
×
357
                        $this->validateHelper->validateFileUuid($data);
×
358
                        $file = $this->fileMapper->getByUuid($data['uuid']);
×
359
                        $this->validateHelper->iRequestedSignThisFile($data['userManager'], $file->getNodeId());
×
360
                } elseif (isset($data['file'])) {
×
361
                        if (!isset($data['file']['fileId'])) {
×
362
                                throw new \Exception($this->l10n->t('Invalid fileID'));
×
363
                        }
364
                        $this->validateHelper->validateLibreSignNodeId($data['file']['fileId']);
×
365
                        $this->validateHelper->iRequestedSignThisFile($data['userManager'], $data['file']['fileId']);
×
366
                } else {
367
                        throw new \Exception($this->l10n->t('Inform or UUID or a File object'));
×
368
                }
369
        }
370

371
        public function validateUsers(array $data): void {
372
                if (empty($data['users'])) {
10✔
373
                        throw new \Exception($this->l10n->t('Empty users list'));
3✔
374
                }
375
                if (!is_array($data['users'])) {
7✔
376
                        throw new \Exception($this->l10n->t('User list needs to be an array'));
1✔
377
                }
378
                $emails = [];
6✔
379
                foreach ($data['users'] as $index => $user) {
6✔
380
                        $this->validateHelper->haveValidMail($user);
6✔
381
                        $identifyMethod = $this->getUserIdentifyMethod($user);
6✔
382
                        $this->validateHelper->validateIdentifyMethod($identifyMethod);
6✔
383
                        $emails[$index] = strtolower($this->getUserEmail($user));
6✔
384
                }
385
                $uniques = array_unique($emails);
6✔
386
                if (count($emails) > count($uniques)) {
6✔
387
                        throw new \Exception($this->l10n->t('Remove duplicated users, email address need to be unique'));
1✔
388
                }
389
        }
390

391
        private function getUserEmail(array $user): string {
392
                if (!empty($user['email'])) {
21✔
393
                        return strtolower($user['email']);
21✔
394
                }
395
                if (!empty($user['uid'])) {
×
396
                        $user = $this->userManager->get($user['uid']);
×
397
                        return $user->getEMailAddress() ?? '';
×
398
                }
399
                return '';
×
400
        }
401

402
        /**
403
         * Can delete sing request
404
         *
405
         * @param array $data
406
         */
407
        public function canDeleteSignRequest(array $data): void {
408
                if (!empty($data['uuid'])) {
6✔
409
                        $signatures = $this->fileUserMapper->getByFileUuid($data['uuid']);
5✔
410
                } elseif (!empty($data['file']['fileId'])) {
1✔
411
                        $signatures = $this->fileUserMapper->getByNodeId($data['file']['fileId']);
1✔
412
                } else {
413
                        throw new \Exception($this->l10n->t('Inform or UUID or a File object'));
×
414
                }
415
                $signed = array_filter($signatures, fn ($s) => $s->getSigned());
6✔
416
                if ($signed) {
6✔
417
                        throw new \Exception($this->l10n->t('Document already signed'));
1✔
418
                }
419
                array_walk($data['users'], function ($user) use ($signatures) {
5✔
420
                        $exists = array_filter($signatures, fn ($s) => $s->getEmail() === $user['email']);
5✔
421
                        if (!$exists) {
5✔
422
                                throw new \Exception($this->l10n->t('No signature was requested to %s', $user['email']));
1✔
423
                        }
424
                });
5✔
425
        }
426

427
        /**
428
         * @deprecated 2.4.0
429
         *
430
         * @param array $data
431
         *
432
         * @return \OCP\AppFramework\Db\Entity[]
433
         *
434
         * @psalm-return list<\OCP\AppFramework\Db\Entity>
435
         */
436
        public function deleteSignRequestDeprecated(array $data): array {
437
                $this->validateHelper->validateFileUuid($data);
3✔
438
                $this->validateUsers($data);
3✔
439
                $this->canDeleteSignRequest($data);
3✔
440

441
                if (!empty($data['uuid'])) {
3✔
442
                        $signatures = $this->fileUserMapper->getByFileUuid($data['uuid']);
2✔
443
                        $fileData = $this->fileMapper->getByUuid($data['uuid']);
2✔
444
                } elseif (!empty($data['file']['fileId'])) {
1✔
445
                        $signatures = $this->fileUserMapper->getByNodeId($data['file']['fileId']);
1✔
446
                        $fileData = $this->fileMapper->getByFileId($data['file']['fileId']);
1✔
447
                } else {
448
                        throw new \Exception($this->l10n->t('Inform or UUID or a File object'));
×
449
                }
450

451
                $deletedUsers = [];
3✔
452
                foreach ($data['users'] as $signer) {
3✔
453
                        try {
454
                                $fileUser = $this->fileUserMapper->getByEmailAndFileId(
3✔
455
                                        $signer['email'],
3✔
456
                                        $fileData->getId()
3✔
457
                                );
3✔
458
                                $deletedUsers[] = $fileUser;
3✔
459
                                $this->fileUserMapper->delete($fileUser);
3✔
460
                        } catch (\Throwable $th) {
2✔
461
                                // already deleted
462
                        }
463
                }
464
                if ((empty($data['users']) && !count($signatures)) || count($signatures) === count($data['users'])) {
3✔
465
                        $this->fileMapper->delete($fileData);
3✔
466
                }
467
                return $deletedUsers;
3✔
468
        }
469

470
        /**
471
         * @param array $data
472
         * @return void
473
         */
474
        public function deleteSignRequest(array $data): void {
475
                if (!empty($data['uuid'])) {
1✔
476
                        $signatures = $this->fileUserMapper->getByFileUuid($data['uuid']);
×
477
                        $fileData = $this->fileMapper->getByUuid($data['uuid']);
×
478
                } elseif (!empty($data['file']['fileId'])) {
1✔
479
                        $signatures = $this->fileUserMapper->getByNodeId($data['file']['fileId']);
1✔
480
                        $fileData = $this->fileMapper->getByFileId($data['file']['fileId']);
1✔
481
                } else {
482
                        throw new \Exception($this->l10n->t('Inform or UUID or a File object'));
×
483
                }
484
                foreach ($signatures as $fileUser) {
1✔
485
                        $this->fileUserMapper->delete($fileUser);
1✔
486
                }
487
                $this->fileMapper->delete($fileData);
1✔
488
                $this->fileElementService->deleteVisibleElements($fileData->getId());
1✔
489
        }
490

491
        public function unassociateToUser(int $fileId, int $fileUserId): void {
492
                $fileUser = $this->fileUserMapper->getByFileIdAndFileUserId($fileId, $fileUserId);
1✔
493
                try {
494
                        $this->fileUserMapper->delete($fileUser);
1✔
495
                        $visibleElements = $this->fileElementMapper->getByFileIdAndUserId($fileId, $fileUser->getUserId());
1✔
496
                        foreach ($visibleElements as $visibleElement) {
×
497
                                $this->fileElementMapper->delete($visibleElement);
×
498
                        }
499
                } catch (\Throwable $th) {
1✔
500
                }
501
        }
502

503
        /**
504
         * @psalm-suppress MixedReturnStatement
505
         * @psalm-suppress MixedMethodCall
506
         */
507
        public function notifyCallback(File $file): void {
508
                $uri = $this->libreSignFile->getCallback();
2✔
509
                if (!$uri) {
2✔
510
                        $uri = $this->config->getAppValue(Application::APP_ID, 'webhook_sign_url');
1✔
511
                        if (!$uri) {
1✔
512
                                return;
1✔
513
                        }
514
                }
515
                $options = [
1✔
516
                        'multipart' => [
1✔
517
                                [
1✔
518
                                        'name' => 'uuid',
1✔
519
                                        'contents' => $this->libreSignFile->getUuid(),
1✔
520
                                ],
1✔
521
                                [
1✔
522
                                        'name' => 'status',
1✔
523
                                        'contents' => $this->libreSignFile->getStatus(),
1✔
524
                                ],
1✔
525
                                [
1✔
526
                                        'name' => 'file',
1✔
527
                                        'contents' => $file->fopen('r'),
1✔
528
                                        'filename' => $file->getName()
1✔
529
                                ]
1✔
530
                        ]
1✔
531
                ];
1✔
532
                $this->client->newClient()->post($uri, $options);
1✔
533
        }
534

535
        /**
536
         * @return static
537
         */
538
        public function setLibreSignFile(FileEntity $libreSignFile): self {
539
                $this->libreSignFile = $libreSignFile;
6✔
540
                return $this;
6✔
541
        }
542

543
        /**
544
         * @return static
545
         */
546
        public function setFileUser(FileUserEntity $fileUser): self {
547
                $this->fileUser = $fileUser;
5✔
548
                return $this;
5✔
549
        }
550

551
        /**
552
         * @return static
553
         */
554
        public function setSignWithoutPassword(bool $signWithoutPassword): self {
555
                $this->signWithoutPassword = $signWithoutPassword;
4✔
556
                return $this;
4✔
557
        }
558

559
        /**
560
         * @return static
561
         */
562
        public function setPassword(?string $password = null): self {
563
                $this->password = $password;
5✔
564
                return $this;
5✔
565
        }
566

567
        /**
568
         * @return static
569
         */
570
        public function setVisibleElements(array $list): self {
571
                $fileElements = $this->fileElementMapper->getByFileIdAndUserId($this->fileUser->getFileId(), $this->fileUser->getUserId());
4✔
572
                foreach ($fileElements as $fileElement) {
4✔
573
                        $element = array_filter($list, function (array $element) use ($fileElement): bool {
×
574
                                return $element['documentElementId'] === $fileElement->getId();
×
575
                        });
×
576
                        if ($element) {
×
577
                                $c = current($element);
×
578
                                $userElement = $this->userElementMapper->findOne(['id' => $c['profileElementId']]);
×
579
                        } else {
580
                                $userElement = $this->userElementMapper->findOne([
×
581
                                        'user_id' => $this->fileUser->getUserId(),
×
582
                                        'type' => $fileElement->getType(),
×
583
                                ]);
×
584
                        }
585
                        try {
586
                                /** @var \OCP\Files\File[] */
587
                                $node = $this->root->getById($userElement->getFileId());
×
588
                                if (!$node) {
×
589
                                        throw new \Exception('empty');
×
590
                                }
591
                                $node = $node[0];
×
592
                        } catch (\Throwable $th) {
×
593
                                throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
×
594
                        }
595
                        $tempFile = $this->tempManager->getTemporaryFile('.png');
×
596
                        file_put_contents($tempFile, $node->getContent());
×
597
                        $visibleElements = new VisibleElementAssoc(
×
598
                                $fileElement,
×
599
                                $userElement,
×
600
                                $tempFile
×
601
                        );
×
602
                        $this->elements[] = $visibleElements;
×
603
                }
604
                return $this;
4✔
605
        }
606

607
        public function sign(): File {
608
                $fileToSign = $this->getFileToSing($this->libreSignFile);
5✔
609
                $pfxFile = $this->getPfxFile();
3✔
610
                switch ($fileToSign->getExtension()) {
2✔
611
                        case 'pdf':
2✔
612
                                $signedFile = $this->pkcs12Handler
2✔
613
                                        ->setInputFile($fileToSign)
2✔
614
                                        ->setCertificate($pfxFile)
2✔
615
                                        ->setVisibleElements($this->elements)
2✔
616
                                        ->setPassword($this->password)
2✔
617
                                        ->sign();
2✔
618
                                break;
1✔
619
                        default:
620
                                $signedFile = $this->pkcs7Handler
×
621
                                        ->setInputFile($fileToSign)
×
622
                                        ->setCertificate($pfxFile)
×
623
                                        ->setPassword($this->password)
×
624
                                        ->sign();
×
625
                }
626

627
                $this->fileUser->setSigned(time());
1✔
628
                if ($this->fileUser->getId()) {
1✔
629
                        $this->fileUserMapper->update($this->fileUser);
1✔
630
                } else {
631
                        $this->fileUserMapper->insert($this->fileUser);
×
632
                }
633

634
                $this->libreSignFile->setSignedNodeId($signedFile->getId());
1✔
635
                $allSigned = $this->updateStatus();
1✔
636
                $this->fileMapper->update($this->libreSignFile);
1✔
637

638
                $this->eventDispatcher->dispatchTyped(new SignedEvent($this, $signedFile, $allSigned));
1✔
639

640
                return $signedFile;
1✔
641
        }
642

643
        public function storeUserMetadata(array $metadata = []): self {
644
                $collectMetadata = $this->config->getAppValue(Application::APP_ID, 'collect_metadata') ? true : false;
4✔
645
                if (!$collectMetadata || !$metadata) {
4✔
646
                        return $this;
4✔
647
                }
648
                $this->fileUser->setMetadata($metadata);
×
649
                $this->fileUserMapper->update($this->fileUser);
×
650
                return $this;
×
651
        }
652

653
        private function updateStatus(): bool {
654
                $signers = $this->fileUserMapper->getByFileId($this->fileUser->getFileId());
1✔
655
                $total = array_reduce($signers, function ($carry, $signer) {
1✔
656
                        $carry += $signer->getSigned() ? 1 : 0;
1✔
657
                        return $carry;
1✔
658
                });
1✔
659
                if (count($signers) === $total && $this->libreSignFile->getStatus() !== FileEntity::STATUS_SIGNED) {
1✔
660
                        $this->libreSignFile->setStatus(FileEntity::STATUS_SIGNED);
1✔
661
                        return true;
1✔
662
                }
663
                return false;
×
664
        }
665

666
        private function getPfxFile(): \OCP\Files\Node {
667
                if ($this->signWithoutPassword) {
3✔
668
                        $tempPassword = sha1(time());
×
669
                        $this->setPassword($tempPassword);
×
670
                        return $this->pkcs12Handler->generateCertificate(
×
671
                                ['email' => $this->fileUser->getEmail()],
×
672
                                $tempPassword,
×
673
                                $this->fileUser->getUserId(),
×
674
                                true
×
675
                        );
×
676
                }
677
                return $this->pkcs12Handler->getPfx($this->fileUser->getUserId());
3✔
678
        }
679

680
        /**
681
         * Get file to sign
682
         *
683
         * @throws LibresignException
684
         * @param FileEntity $libresignFile
685
         * @return \OCP\Files\Node
686
         */
687
        public function getFileToSing(FileEntity $libresignFile): \OCP\Files\Node {
688
                $userFolder = $this->root->getUserFolder($libresignFile->getUserId());
5✔
689
                $originalFile = $userFolder->getById($libresignFile->getNodeId());
5✔
690
                if (count($originalFile) < 1) {
5✔
691
                        throw new LibresignException($this->l10n->t('File not found'));
2✔
692
                }
693
                $originalFile = $originalFile[0];
3✔
694
                if ($originalFile->getExtension() === 'pdf') {
3✔
695
                        return $this->getPdfToSign($libresignFile, $originalFile);
3✔
696
                }
697
                return $userFolder->get($originalFile);
×
698
        }
699

700
        public function getLibresignFile(?int $fileId, ?string $fileUserUuid): FileEntity {
701
                try {
702
                        if ($fileId) {
7✔
703
                                $libresignFile = $this->fileMapper->getByFileId($fileId);
1✔
704
                        } elseif ($fileUserUuid) {
6✔
705
                                $fileUser = $this->fileUserMapper->getByUuid($fileUserUuid);
6✔
706
                                $libresignFile = $this->fileMapper->getById($fileUser->getFileId());
5✔
707
                        } else {
708
                                throw new \Exception('Invalid arguments');
5✔
709
                        }
710
                } catch (DoesNotExistException $th) {
2✔
711
                        throw new LibresignException($this->l10n->t('File not found'), 1);
2✔
712
                }
713
                return $libresignFile;
5✔
714
        }
715

716
        public function requestCode(FileUserEntity $fileUser, IUser $user): string {
717
                $token = $this->secureRandom->generate(6, ISecureRandom::CHAR_DIGITS);
×
718
                $this->sendCode($user, $fileUser, $token);
×
719
                $fileUser->setCode($this->hasher->hash($token));
×
720
                $this->fileUserMapper->update($fileUser);
×
721
                return $token;
×
722
        }
723

724
        private function sendCode(IUser $user, FileUserEntity $fileUser, string $code): void {
725
                $signMethod = $this->config->getAppValue(Application::APP_ID, 'sign_method', 'password');
×
726
                switch ($signMethod) {
727
                        case 'sms':
×
728
                        case 'telegram':
×
729
                        case 'signal':
×
730
                                $this->sendCodeByGateway($user, $code, $signMethod);
×
731
                                break;
×
732
                        case 'email':
×
733
                                $this->sendCodeByEmail($fileUser, $code);
×
734
                                break;
×
735
                        case 'password':
×
736
                                throw new LibresignException($this->l10n->t('Sending authorization code not enabled.'));
×
737
                }
738
        }
739

740
        private function sendCodeByGateway(IUser $user, string $code, string $gatewayName): void {
741
                $gateway = $this->getGateway($user, $gatewayName);
×
742
                
743
                $userAccount = $this->accountManager->getAccount($user);
×
744
                $identifier = $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue();
×
745
                $gateway->send($user, $identifier, $this->l10n->t('%s is your LibreSign verification code.', $code));
×
746
        }
747

748
        /**
749
         * @throws OCSForbiddenException
750
         */
751
        private function getGateway(IUser $user, string $gatewayName): \OCA\TwoFactorGateway\Service\Gateway\IGateway {
752
                if (!$this->appManager->isEnabledForUser('twofactor_gateway', $user)) {
×
753
                        throw new OCSForbiddenException($this->l10n->t('Authorize signing using %s token is disabled because Nextcloud Two-Factor Gateway is not enabled.', $gatewayName));
×
754
                }
755
                $factory = $this->serverContainer->get('\OCA\TwoFactorGateway\Service\Gateway\Factory');
×
756
                $gateway = $factory->getGateway($gatewayName);
×
757
                if (!$gateway->getConfig()->isComplete()) {
×
758
                        throw new OCSForbiddenException($this->l10n->t('Gateway %s not configured on Two-Factor Gateway.', $gatewayName));
×
759
                }
760
                return $gateway;
×
761
        }
762

763
        private function sendCodeByEmail(FileUserEntity $fileUser, string $code): void {
764
                $this->mail->sendCodeToSign($fileUser, $code);
×
765
        }
766

767
        public function getFileUserToSign(FileEntity $libresignFile, IUser $user): FileUserEntity {
768
                $this->validateHelper->fileCanBeSigned($libresignFile);
5✔
769
                try {
770
                        $fileUser = $this->fileUserMapper->getByFileIdAndUserId($libresignFile->getNodeId(), $user->getUID());
5✔
771
                        if ($fileUser->getSigned()) {
5✔
772
                                throw new LibresignException($this->l10n->t('File already signed by you'), 1);
5✔
773
                        }
774
                } catch (DoesNotExistException $th) {
1✔
775
                        try {
776
                                $accountFile = $this->accountFileMapper->getByFileId($libresignFile->getId());
×
777
                        } catch (\Throwable $th) {
×
778
                                throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
779
                        }
780
                        $this->validateHelper->userCanApproveValidationDocuments($user);
×
781
                        $fileUser = new FileUserEntity();
×
782
                        $fileUser->setFileId($libresignFile->getId());
×
783
                        $fileUser->setEmail($user->getEMailAddress());
×
784
                        $fileUser->setDisplayName($user->getDisplayName());
×
785
                        $fileUser->setUserId($user->getUID());
×
786
                        $fileUser->setUuid(UUIDUtil::getUUID());
×
787
                        $fileUser->setCreatedAt(time());
×
788
                }
789
                return $fileUser;
4✔
790
        }
791

792
        /**
793
         * @psalm-suppress MixedReturnStatement
794
         * @psalm-suppress InvalidReturnStatement
795
         * @psalm-suppress MixedMethodCall
796
         *
797
         * @return File
798
         */
799
        private function getPdfToSign(FileEntity $fileData, File $originalFile): File {
800
                if ($fileData->getSignedNodeId()) {
3✔
801
                        /** @var \OCP\Files\File */
802
                        $fileToSign = $this->root->getById($fileData->getSignedNodeId())[0];
×
803
                } else {
804
                        $signedFilePath = preg_replace(
3✔
805
                                '/' . $originalFile->getExtension() . '$/',
3✔
806
                                $this->l10n->t('signed') . '.' . $originalFile->getExtension(),
3✔
807
                                $originalFile->getPath()
3✔
808
                        );
3✔
809

810
                        /** @var \OCP\Files\File */
811
                        $buffer = $this->pkcs12Handler->writeFooter($originalFile, $fileData->getUuid());
3✔
812
                        if (!$buffer) {
3✔
813
                                $buffer = $originalFile->getContent($originalFile);
×
814
                        }
815
                        /** @var \OCP\Files\File */
816
                        $fileToSign = $this->root->newFile($signedFilePath);
3✔
817
                        $fileToSign->putContent($buffer);
3✔
818
                }
819
                return $fileToSign;
3✔
820
        }
821

822
        /**
823
         * @return (array|int|mixed)[]
824
         * @psalm-return array{action?: int, user?: array{name: mixed}, sign?: array{pdf: array{file?: File, nodeId?: mixed, url?: mixed, base64?: string}|null, uuid: mixed, filename: mixed, description: mixed}, errors?: non-empty-list<mixed>, redirect?: mixed, settings?: array{accountHash: string}}
825
         */
826
        public function getInfoOfFileToSignUsingFileUserUuid(?string $uuid, ?IUser $user, string $formatOfPdfOnSign): array {
827
                $return = [];
16✔
828
                if (!$uuid) {
16✔
829
                        return $return;
2✔
830
                }
831
                try {
832
                        $fileUser = $this->fileUserMapper->getByUuid($uuid);
14✔
833
                        $fileEntity = $this->fileMapper->getById($fileUser->getFileId());
13✔
834
                } catch (DoesNotExistException $e) {
1✔
835
                        throw new LibresignException(json_encode([
1✔
836
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
837
                                'errors' => [$this->l10n->t('Invalid UUID')],
1✔
838
                        ]));
1✔
839
                }
840
                $this->trhowIfCantIdentifyUser($uuid, $user, $fileUser);
13✔
841
                $this->throwIfUserIsNotSigner($user, $fileUser);
9✔
842
                $this->throwIfAlreadySigned($fileEntity, $fileUser);
8✔
843
                $this->throwIfInvalidUser($uuid, $user);
7✔
844
                $userFolder = $this->root->getUserFolder($fileEntity->getUserId());
6✔
845
                $fileToSign = $userFolder->getById($fileEntity->getNodeId());
6✔
846
                if (count($fileToSign) < 1) {
6✔
847
                        throw new LibresignException(json_encode([
1✔
848
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
849
                                'errors' => [$this->l10n->t('File not found')],
1✔
850
                        ]));
1✔
851
                }
852
                /** @var File */
853
                $fileToSign = $fileToSign[0];
5✔
854
                $return = $this->getFileData($fileEntity, $user, $fileUser);
5✔
855
                $return['sign']['pdf'] = $this->getFileUrl($formatOfPdfOnSign, $fileEntity, $fileToSign, $uuid);
5✔
856
                return $return;
5✔
857
        }
858

859
        public function getInfoOfFileToSignUsingFileUuid(?string $uuid, ?IUser $user, string $formatOfPdfOnSign): array {
860
                $return = [];
×
861
                if (!$uuid) {
×
862
                        return $return;
×
863
                }
864
                try {
865
                        $fileEntity = $this->fileMapper->getByUuid($uuid);
×
866
                        $this->accountFileMapper->getByFileId($fileEntity->getId());
×
867
                } catch (DoesNotExistException $e) {
×
868
                        throw new LibresignException(json_encode([
×
869
                                'action' => JSActions::ACTION_DO_NOTHING,
×
870
                                'errors' => [$this->l10n->t('Invalid UUID')],
×
871
                        ]));
×
872
                }
873
                $this->throwIfAlreadySigned($fileEntity);
×
874
                try {
875
                        $this->validateHelper->userCanApproveValidationDocuments($user);
×
876
                } catch (LibresignException $e) {
×
877
                        throw new LibresignException(json_encode([
×
878
                                'action' => JSActions::ACTION_DO_NOTHING,
×
879
                                'errors' => [$e->getMessage()],
×
880
                        ]));
×
881
                }
882
                $userFolder = $this->root->getUserFolder($fileEntity->getUserId());
×
883
                $fileToSign = $userFolder->getById($fileEntity->getNodeId());
×
884
                if (count($fileToSign) < 1) {
×
885
                        throw new LibresignException(json_encode([
×
886
                                'action' => JSActions::ACTION_DO_NOTHING,
×
887
                                'errors' => [$this->l10n->t('File not found')],
×
888
                        ]));
×
889
                }
890
                /** @var File */
891
                $fileToSign = $fileToSign[0];
×
892
                $return = $this->getFileData($fileEntity, $user);
×
893
                $return['sign']['pdf'] = $this->getFileUrl($formatOfPdfOnSign, $fileEntity, $fileToSign, $uuid);
×
894
                return $return;
×
895
        }
896

897
        private function throwIfInvalidUser(string $uuid, ?IUser $user): void {
898
                if (!$user) {
7✔
899
                        throw new LibresignException(json_encode([
1✔
900
                                'action' => JSActions::ACTION_REDIRECT,
1✔
901
                                'errors' => [$this->l10n->t('You are not logged in. Please log in.')],
1✔
902
                                'redirect' => $this->urlGenerator->linkToRoute('core.login.showLoginForm', [
1✔
903
                                        'redirect_url' => $this->urlGenerator->linkToRoute(
1✔
904
                                                'libresign.page.sign',
1✔
905
                                                ['uuid' => $uuid]
1✔
906
                                        ),
1✔
907
                                ]),
1✔
908
                        ]));
1✔
909
                }
910
        }
911

912
        private function throwIfAlreadySigned(FileEntity $fileEntity, ?FileUserEntity $fileUser = null): void {
913
                if ($fileEntity->getStatus() === FileEntity::STATUS_SIGNED
8✔
914
                        || (!is_null($fileUser) && $fileUser->getSigned())
8✔
915
                ) {
916
                        throw new LibresignException(json_encode([
1✔
917
                                'action' => JSActions::ACTION_SHOW_ERROR,
1✔
918
                                'errors' => [$this->l10n->t('File already signed.')],
1✔
919
                                'uuid' => $fileEntity->getUuid(),
1✔
920
                        ]));
1✔
921
                }
922
        }
923

924
        private function trhowIfCantIdentifyUser(string $uuid, ?IUser $user, ?FileUserEntity $fileUser): void {
925
                if ($fileUser instanceof FileUserEntity) {
13✔
926
                        $fileUserId = $fileUser->getUserId();
13✔
927
                        if ($fileUserId) {
13✔
928
                                return;
9✔
929
                        }
930
                }
931
                if ($user) {
4✔
932
                        throw new LibresignException(json_encode([
2✔
933
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
934
                                'errors' => [$this->l10n->t('This is not your file')],
2✔
935
                        ]));
2✔
936
                }
937
                $email = $fileUser->getEmail();
2✔
938
                if ($this->userManager->getByEmail($email)) {
2✔
939
                        throw new LibresignException(json_encode([
1✔
940
                                'action' => JSActions::ACTION_REDIRECT,
1✔
941
                                'errors' => [$this->l10n->t('User already exists. Please login.')],
1✔
942
                                'redirect' => $this->urlGenerator->linkToRoute('core.login.showLoginForm', [
1✔
943
                                        'redirect_url' => $this->urlGenerator->linkToRoute(
1✔
944
                                                'libresign.page.sign',
1✔
945
                                                ['uuid' => $uuid]
1✔
946
                                        ),
1✔
947
                                ]),
1✔
948
                        ]));
1✔
949
                }
950
                throw new LibresignException(json_encode([
1✔
951
                        'action' => JSActions::ACTION_CREATE_USER,
1✔
952
                        'settings' => ['accountHash' => md5($email)],
1✔
953
                ]));
1✔
954
        }
955

956
        private function throwIfUserIsNotSigner(?IUser $user, FileUserEntity $fileUser): void {
957
                if ($user && $fileUser->getUserId() !== $user->getUID()) {
9✔
958
                        throw new LibresignException(json_encode([
1✔
959
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
960
                                'errors' => [$this->l10n->t('Invalid user')],
1✔
961
                        ]));
1✔
962
                }
963
        }
964

965
        private function getFileData(FileEntity $fileData, IUser $user, ?FileUserEntity $fileUser = null): array {
966
                $return['action'] = JSActions::ACTION_SIGN;
5✔
967
                $return['sign'] = [
5✔
968
                        'uuid' => $fileData->getUuid(),
5✔
969
                        'filename' => $fileData->getName()
5✔
970
                ];
5✔
971
                if ($fileUser) {
5✔
972
                        $return['user']['name'] = $fileUser->getDisplayName();
5✔
973
                        $return['sign']['description'] = $fileUser->getDescription();
5✔
974
                } else {
975
                        $return['user']['name'] = $user->getDisplayName();
×
976
                }
977
                return $return;
5✔
978
        }
979

980
        /**
981
         * @return array
982
         *
983
         * @psalm-return array{file?: File, nodeId?: int, url?: string, base64?: string}
984
         */
985
        private function getFileUrl(string $format, FileEntity $fileEntity, File $fileToSign, string $uuid): array {
986
                $url = [];
5✔
987
                switch ($format) {
988
                        case 'base64':
5✔
989
                                $url = ['base64' => base64_encode($fileToSign->getContent())];
1✔
990
                                break;
1✔
991
                        case 'url':
4✔
992
                                try {
993
                                        $this->accountFileMapper->getByFileId($fileEntity->getId());
1✔
994
                                        $url = ['url' => $this->urlGenerator->linkToRoute('libresign.page.getPdf', ['uuid' => $uuid])];
1✔
995
                                } catch (DoesNotExistException $e) {
×
996
                                        $url = ['url' => $this->urlGenerator->linkToRoute('libresign.page.getPdfUser', ['uuid' => $uuid])];
×
997
                                }
998
                                break;
1✔
999
                        case 'nodeId':
3✔
1000
                                $url = ['nodeId' => $fileToSign->getId()];
1✔
1001
                                break;
1✔
1002
                        case 'file':
2✔
1003
                                $url = ['file' => $fileToSign];
2✔
1004
                                break;
2✔
1005
                }
1006
                return $url;
5✔
1007
        }
1008
}
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