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

LibreSign / libresign / 4660867154

pending completion
4660867154

push

github

GitHub
Merge pull request #1607 from LibreSign/backport/1584/stable26

9 of 14 new or added lines in 4 files covered. (64.29%)

1 existing line in 1 file now uncovered.

2595 of 4505 relevant lines covered (57.6%)

4.77 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
                        // TRANSLATION This message will be displayed when the request to API with the key users has a value that is not an array
377
                        throw new \Exception($this->l10n->t('User list needs to be an array'));
1✔
378
                }
379
                $emails = [];
6✔
380
                foreach ($data['users'] as $index => $user) {
6✔
381
                        $this->validateHelper->haveValidMail($user);
6✔
382
                        $identifyMethod = $this->getUserIdentifyMethod($user);
6✔
383
                        $this->validateHelper->validateIdentifyMethod($identifyMethod);
6✔
384
                        $emails[$index] = strtolower($this->getUserEmail($user));
6✔
385
                }
386
                $uniques = array_unique($emails);
6✔
387
                if (count($emails) > count($uniques)) {
6✔
388
                        throw new \Exception($this->l10n->t('Remove duplicated users, email address need to be unique'));
1✔
389
                }
390
        }
391

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

641
                return $signedFile;
1✔
642
        }
643

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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