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

LibreSign / libresign / 3717593689

pending completion
3717593689

Pull #1287

github

GitHub
Merge 42fe701e4 into ff8c1edaf
Pull Request #1287: [stable25] Bump packages

2548 of 4384 relevant lines covered (58.12%)

4.74 hits per line

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

72.02
/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->getPath());
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
        /**
283
         * @psalm-suppress MixedReturnStatement
284
         */
285
        private function getFileUser(string $email, int $fileId): FileUserEntity {
286
                try {
287
                        $fileUser = $this->fileUserMapper->getByEmailAndFileId($email, $fileId);
17✔
288
                } catch (DoesNotExistException $e) {
16✔
289
                        $fileUser = new FileUserEntity();
16✔
290
                }
291
                return $fileUser;
17✔
292
        }
293

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

325
        public function validateNewRequestToFile(array $data): void {
326
                $this->validateUserManager($data);
9✔
327
                $this->validateNewFile($data);
8✔
328
                $this->validateUsers($data);
7✔
329
                $this->validateHelper->validateFileStatus($data);
2✔
330
        }
331

332
        public function validateUserManager(array $user): void {
333
                if (!isset($user['userManager'])) {
19✔
334
                        throw new \Exception($this->l10n->t('You are not allowed to request signing'), Http::STATUS_UNPROCESSABLE_ENTITY);
1✔
335
                }
336
                $this->validateHelper->canRequestSign($user['userManager']);
18✔
337
        }
338

339
        public function validateNewFile(array $data): void {
340
                if (empty($data['name'])) {
8✔
341
                        throw new \Exception($this->l10n->t('Name is mandatory'));
1✔
342
                }
343
                $this->validateHelper->validateNewFile($data);
7✔
344
        }
345

346
        public function validateExistingFile(array $data): void {
347
                if (isset($data['uuid'])) {
×
348
                        $this->validateHelper->validateFileUuid($data);
×
349
                        $file = $this->fileMapper->getByUuid($data['uuid']);
×
350
                        $this->validateHelper->iRequestedSignThisFile($data['userManager'], $file->getNodeId());
×
351
                } elseif (isset($data['file'])) {
×
352
                        if (!isset($data['file']['fileId'])) {
×
353
                                throw new \Exception($this->l10n->t('Invalid fileID'));
×
354
                        }
355
                        $this->validateHelper->validateLibreSignNodeId($data['file']['fileId']);
×
356
                        $this->validateHelper->iRequestedSignThisFile($data['userManager'], $data['file']['fileId']);
×
357
                } else {
358
                        throw new \Exception($this->l10n->t('Inform or UUID or a File object'));
×
359
                }
360
        }
361

362
        public function validateUsers(array $data): void {
363
                if (empty($data['users'])) {
10✔
364
                        throw new \Exception($this->l10n->t('Empty users list'));
3✔
365
                }
366
                if (!is_array($data['users'])) {
7✔
367
                        throw new \Exception($this->l10n->t('User list needs to be an array'));
1✔
368
                }
369
                $emails = [];
6✔
370
                foreach ($data['users'] as $index => $user) {
6✔
371
                        $this->validateHelper->haveValidMail($user);
6✔
372
                        $emails[$index] = strtolower($this->getUserEmail($user));
6✔
373
                }
374
                $uniques = array_unique($emails);
6✔
375
                if (count($emails) > count($uniques)) {
6✔
376
                        throw new \Exception($this->l10n->t('Remove duplicated users, email address need to be unique'));
1✔
377
                }
378
        }
379

380
        private function getUserEmail(array $user): string {
381
                if (!empty($user['email'])) {
21✔
382
                        return strtolower($user['email']);
21✔
383
                }
384
                if (!empty($user['uid'])) {
×
385
                        $user = $this->userManager->get($user['uid']);
×
386
                        return $user->getEMailAddress() ?? '';
×
387
                }
388
                return '';
×
389
        }
390

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

416
        /**
417
         * @deprecated 2.4.0
418
         *
419
         * @param array $data
420
         *
421
         * @return \OCP\AppFramework\Db\Entity[]
422
         *
423
         * @psalm-return list<\OCP\AppFramework\Db\Entity>
424
         */
425
        public function deleteSignRequestDeprecated(array $data): array {
426
                $this->validateHelper->validateFileUuid($data);
3✔
427
                $this->validateUsers($data);
3✔
428
                $this->canDeleteSignRequest($data);
3✔
429

430
                if (!empty($data['uuid'])) {
3✔
431
                        $signatures = $this->fileUserMapper->getByFileUuid($data['uuid']);
2✔
432
                        $fileData = $this->fileMapper->getByUuid($data['uuid']);
2✔
433
                } elseif (!empty($data['file']['fileId'])) {
1✔
434
                        $signatures = $this->fileUserMapper->getByNodeId($data['file']['fileId']);
1✔
435
                        $fileData = $this->fileMapper->getByFileId($data['file']['fileId']);
1✔
436
                } else {
437
                        throw new \Exception($this->l10n->t('Inform or UUID or a File object'));
×
438
                }
439

440
                $deletedUsers = [];
3✔
441
                foreach ($data['users'] as $signer) {
3✔
442
                        try {
443
                                $fileUser = $this->fileUserMapper->getByEmailAndFileId(
3✔
444
                                        $signer['email'],
3✔
445
                                        $fileData->getId()
3✔
446
                                );
3✔
447
                                $deletedUsers[] = $fileUser;
3✔
448
                                $this->fileUserMapper->delete($fileUser);
3✔
449
                        } catch (\Throwable $th) {
2✔
450
                                // already deleted
451
                        }
452
                }
453
                if ((empty($data['users']) && !count($signatures)) || count($signatures) === count($data['users'])) {
3✔
454
                        $this->fileMapper->delete($fileData);
3✔
455
                }
456
                return $deletedUsers;
3✔
457
        }
458

459
        /**
460
         * @param array $data
461
         * @return void
462
         */
463
        public function deleteSignRequest(array $data): void {
464
                if (!empty($data['uuid'])) {
1✔
465
                        $signatures = $this->fileUserMapper->getByFileUuid($data['uuid']);
×
466
                        $fileData = $this->fileMapper->getByUuid($data['uuid']);
×
467
                } elseif (!empty($data['file']['fileId'])) {
1✔
468
                        $signatures = $this->fileUserMapper->getByNodeId($data['file']['fileId']);
1✔
469
                        $fileData = $this->fileMapper->getByFileId($data['file']['fileId']);
1✔
470
                } else {
471
                        throw new \Exception($this->l10n->t('Inform or UUID or a File object'));
×
472
                }
473
                foreach ($signatures as $fileUser) {
1✔
474
                        $this->fileUserMapper->delete($fileUser);
1✔
475
                }
476
                $this->fileMapper->delete($fileData);
1✔
477
                $this->fileElementService->deleteVisibleElements($fileData->getId());
1✔
478
        }
479

480
        public function unassociateToUser(int $fileId, int $fileUserId): void {
481
                $fileUser = $this->fileUserMapper->getByFileIdAndFileUserId($fileId, $fileUserId);
1✔
482
                try {
483
                        $this->fileUserMapper->delete($fileUser);
1✔
484
                        $visibleElements = $this->fileElementMapper->getByFileIdAndUserId($fileId, $fileUser->getUserId());
1✔
485
                        foreach ($visibleElements as $visibleElement) {
×
486
                                $this->fileElementMapper->delete($visibleElement);
×
487
                        }
488
                } catch (\Throwable $th) {
1✔
489
                }
490
        }
491

492
        /**
493
         * @psalm-suppress MixedReturnStatement
494
         * @psalm-suppress MixedMethodCall
495
         */
496
        public function notifyCallback(File $file): void {
497
                $uri = $this->libreSignFile->getCallback();
2✔
498
                if (!$uri) {
2✔
499
                        $uri = $this->config->getAppValue(Application::APP_ID, 'webhook_sign_url');
1✔
500
                        if (!$uri) {
1✔
501
                                return;
1✔
502
                        }
503
                }
504
                $options = [
1✔
505
                        'multipart' => [
1✔
506
                                [
1✔
507
                                        'name' => 'uuid',
1✔
508
                                        'contents' => $this->libreSignFile->getUuid(),
1✔
509
                                ],
1✔
510
                                [
1✔
511
                                        'name' => 'status',
1✔
512
                                        'contents' => $this->libreSignFile->getStatus(),
1✔
513
                                ],
1✔
514
                                [
1✔
515
                                        'name' => 'file',
1✔
516
                                        'contents' => $file->fopen('r'),
1✔
517
                                        'filename' => $file->getName()
1✔
518
                                ]
1✔
519
                        ]
1✔
520
                ];
1✔
521
                $this->client->newClient()->post($uri, $options);
1✔
522
        }
523

524
        /**
525
         * @return static
526
         */
527
        public function setLibreSignFile(FileEntity $libreSignFile): self {
528
                $this->libreSignFile = $libreSignFile;
6✔
529
                return $this;
6✔
530
        }
531

532
        /**
533
         * @return static
534
         */
535
        public function setFileUser(FileUserEntity $fileUser): self {
536
                $this->fileUser = $fileUser;
5✔
537
                return $this;
5✔
538
        }
539

540
        /**
541
         * @return static
542
         */
543
        public function setSignWithoutPassword(bool $signWithoutPassword): self {
544
                $this->signWithoutPassword = $signWithoutPassword;
4✔
545
                return $this;
4✔
546
        }
547

548
        /**
549
         * @return static
550
         */
551
        public function setPassword(?string $password = null): self {
552
                $this->password = $password;
5✔
553
                return $this;
5✔
554
        }
555

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

596
        public function sign(): File {
597
                $fileToSign = $this->getFileToSing($this->libreSignFile);
5✔
598
                $pfxFile = $this->getPfxFile();
3✔
599
                switch ($fileToSign->getExtension()) {
2✔
600
                        case 'pdf':
2✔
601
                                $signedFile = $this->pkcs12Handler
2✔
602
                                        ->setInputFile($fileToSign)
2✔
603
                                        ->setCertificate($pfxFile)
2✔
604
                                        ->setVisibleElements($this->elements)
2✔
605
                                        ->setPassword($this->password)
2✔
606
                                        ->sign();
2✔
607
                                break;
1✔
608
                        default:
609
                                $signedFile = $this->pkcs7Handler
×
610
                                        ->setInputFile($fileToSign)
×
611
                                        ->setCertificate($pfxFile)
×
612
                                        ->setPassword($this->password)
×
613
                                        ->sign();
×
614
                }
615

616
                $this->fileUser->setSigned(time());
1✔
617
                if ($this->fileUser->getId()) {
1✔
618
                        $this->fileUserMapper->update($this->fileUser);
1✔
619
                } else {
620
                        $this->fileUserMapper->insert($this->fileUser);
×
621
                }
622

623
                $this->libreSignFile->setSignedNodeId($signedFile->getId());
1✔
624
                $allSigned = $this->updateStatus();
1✔
625
                $this->fileMapper->update($this->libreSignFile);
1✔
626

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

629
                return $signedFile;
1✔
630
        }
631

632
        private function updateStatus(): bool {
633
                $signers = $this->fileUserMapper->getByFileId($this->fileUser->getFileId());
1✔
634
                $total = array_reduce($signers, function ($carry, $signer) {
1✔
635
                        $carry += $signer->getSigned() ? 1 : 0;
1✔
636
                        return $carry;
1✔
637
                });
1✔
638
                if (count($signers) === $total && $this->libreSignFile->getStatus() !== FileEntity::STATUS_SIGNED) {
1✔
639
                        $this->libreSignFile->setStatus(FileEntity::STATUS_SIGNED);
1✔
640
                        return true;
1✔
641
                }
642
                return false;
×
643
        }
644

645
        private function getPfxFile(): \OCP\Files\Node {
646
                if ($this->signWithoutPassword) {
3✔
647
                        $tempPassword = sha1(time());
×
648
                        $this->setPassword($tempPassword);
×
649
                        return $this->pkcs12Handler->generateCertificate(
×
650
                                ['email' => $this->fileUser->getEmail()],
×
651
                                $tempPassword,
×
652
                                $this->fileUser->getUserId(),
×
653
                                true
×
654
                        );
×
655
                }
656
                return $this->pkcs12Handler->getPfx($this->fileUser->getUserId());
3✔
657
        }
658

659
        /**
660
         * Get file to sign
661
         *
662
         * @throws LibresignException
663
         * @param FileEntity $libresignFile
664
         * @return \OCP\Files\Node
665
         */
666
        public function getFileToSing(FileEntity $libresignFile): \OCP\Files\Node {
667
                $userFolder = $this->root->getUserFolder($libresignFile->getUserId());
5✔
668
                $originalFile = $userFolder->getById($libresignFile->getNodeId());
5✔
669
                if (count($originalFile) < 1) {
5✔
670
                        throw new LibresignException($this->l10n->t('File not found'));
2✔
671
                }
672
                $originalFile = $originalFile[0];
3✔
673
                if ($originalFile->getExtension() === 'pdf') {
3✔
674
                        return $this->getPdfToSign($libresignFile, $originalFile);
3✔
675
                }
676
                return $userFolder->get($originalFile);
×
677
        }
678

679
        public function getLibresignFile(?int $fileId, ?string $fileUserUuid): FileEntity {
680
                try {
681
                        if ($fileId) {
7✔
682
                                $libresignFile = $this->fileMapper->getByFileId($fileId);
1✔
683
                        } elseif ($fileUserUuid) {
6✔
684
                                $fileUser = $this->fileUserMapper->getByUuid($fileUserUuid);
6✔
685
                                $libresignFile = $this->fileMapper->getById($fileUser->getFileId());
5✔
686
                        } else {
687
                                throw new \Exception('Invalid arguments');
5✔
688
                        }
689
                } catch (DoesNotExistException $th) {
2✔
690
                        throw new LibresignException($this->l10n->t('File not found'), 1);
2✔
691
                }
692
                return $libresignFile;
5✔
693
        }
694

695
        public function requestCode(FileUserEntity $fileUser, IUser $user): string {
696
                $token = $this->secureRandom->generate(6, ISecureRandom::CHAR_DIGITS);
×
697
                $this->sendCode($user, $fileUser, $token);
×
698
                $fileUser->setCode($this->hasher->hash($token));
×
699
                $this->fileUserMapper->update($fileUser);
×
700
                return $token;
×
701
        }
702

703
        private function sendCode(IUser $user, FileUserEntity $fileUser, string $code): void {
704
                $signMethod = $this->config->getAppValue(Application::APP_ID, 'sign_method', 'password');
×
705
                switch ($signMethod) {
706
                        case 'sms':
×
707
                        case 'telegram':
×
708
                        case 'signal':
×
709
                                $this->sendCodeByGateway($user, $code, $signMethod);
×
710
                                break;
×
711
                        case 'email':
×
712
                                $this->sendCodeByEmail($fileUser, $code);
×
713
                                break;
×
714
                        case 'password':
×
715
                                throw new LibresignException($this->l10n->t('Sending authorization code not enabled.'));
×
716
                }
717
        }
718

719
        private function sendCodeByGateway(IUser $user, string $code, string $gatewayName): void {
720
                $gateway = $this->getGateway($user, $gatewayName);
×
721
                
722
                $userAccount = $this->accountManager->getAccount($user);
×
723
                $identifier = $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue();
×
724
                $gateway->send($user, $identifier, $this->l10n->t('%s is your LibreSign verification code.', $code));
×
725
        }
726

727
        /**
728
         * @throws OCSForbiddenException
729
         */
730
        private function getGateway(IUser $user, string $gatewayName): \OCA\TwoFactorGateway\Service\Gateway\IGateway {
731
                if (!$this->appManager->isEnabledForUser('twofactor_gateway', $user)) {
×
732
                        throw new OCSForbiddenException($this->l10n->t('Authorize signing using %s token is disabled because Nextcloud Two-Factor Gateway is not enabled.', $gatewayName));
×
733
                }
734
                $factory = $this->serverContainer->get('\OCA\TwoFactorGateway\Service\Gateway\Factory');
×
735
                $gateway = $factory->getGateway($gatewayName);
×
736
                if (!$gateway->getConfig()->isComplete()) {
×
737
                        throw new OCSForbiddenException($this->l10n->t('Gateway %s not configured on Two-Factor Gateway.', $gatewayName));
×
738
                }
739
                return $gateway;
×
740
        }
741

742
        private function sendCodeByEmail(FileUserEntity $fileUser, string $code): void {
743
                $this->mail->sendCodeToSign($fileUser, $code);
×
744
        }
745

746
        public function getFileUserToSign(FileEntity $libresignFile, IUser $user): FileUserEntity {
747
                $this->validateHelper->fileCanBeSigned($libresignFile);
5✔
748
                try {
749
                        $fileUser = $this->fileUserMapper->getByFileIdAndUserId($libresignFile->getNodeId(), $user->getUID());
5✔
750
                        if ($fileUser->getSigned()) {
5✔
751
                                throw new LibresignException($this->l10n->t('File already signed by you'), 1);
5✔
752
                        }
753
                } catch (DoesNotExistException $th) {
1✔
754
                        try {
755
                                $accountFile = $this->accountFileMapper->getByFileId($libresignFile->getId());
×
756
                        } catch (\Throwable $th) {
×
757
                                throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
758
                        }
759
                        $this->validateHelper->userCanApproveValidationDocuments($user);
×
760
                        $fileUser = new FileUserEntity();
×
761
                        $fileUser->setFileId($libresignFile->getId());
×
762
                        $fileUser->setEmail($user->getEMailAddress());
×
763
                        $fileUser->setDisplayName($user->getDisplayName());
×
764
                        $fileUser->setUserId($user->getUID());
×
765
                        $fileUser->setUuid(UUIDUtil::getUUID());
×
766
                        $fileUser->setCreatedAt(time());
×
767
                }
768
                return $fileUser;
4✔
769
        }
770

771
        /**
772
         * @psalm-suppress MixedReturnStatement
773
         * @psalm-suppress InvalidReturnStatement
774
         * @psalm-suppress MixedMethodCall
775
         *
776
         * @return File
777
         */
778
        private function getPdfToSign(FileEntity $fileData, File $originalFile): File {
779
                if ($fileData->getSignedNodeId()) {
3✔
780
                        /** @var \OCP\Files\File */
781
                        $fileToSign = $this->root->getById($fileData->getSignedNodeId())[0];
×
782
                } else {
783
                        $signedFilePath = preg_replace(
3✔
784
                                '/' . $originalFile->getExtension() . '$/',
3✔
785
                                $this->l10n->t('signed') . '.' . $originalFile->getExtension(),
3✔
786
                                $originalFile->getPath()
3✔
787
                        );
3✔
788

789
                        /** @var \OCP\Files\File */
790
                        $buffer = $this->pkcs12Handler->writeFooter($originalFile, $fileData->getUuid());
3✔
791
                        if (!$buffer) {
3✔
792
                                $buffer = $originalFile->getContent($originalFile);
×
793
                        }
794
                        /** @var \OCP\Files\File */
795
                        $fileToSign = $this->root->newFile($signedFilePath);
3✔
796
                        $fileToSign->putContent($buffer);
3✔
797
                }
798
                return $fileToSign;
3✔
799
        }
800

801
        /**
802
         * @return (array|int|mixed)[]
803
         * @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}}
804
         */
805
        public function getInfoOfFileToSignUsingFileUserUuid(?string $uuid, ?IUser $user, string $formatOfPdfOnSign): array {
806
                $return = [];
16✔
807
                if (!$uuid) {
16✔
808
                        return $return;
2✔
809
                }
810
                try {
811
                        $fileUser = $this->fileUserMapper->getByUuid($uuid);
14✔
812
                        $fileEntity = $this->fileMapper->getById($fileUser->getFileId());
13✔
813
                } catch (DoesNotExistException $e) {
1✔
814
                        throw new LibresignException(json_encode([
1✔
815
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
816
                                'errors' => [$this->l10n->t('Invalid UUID')],
1✔
817
                        ]));
1✔
818
                }
819
                $this->trhowIfFileUserNotExists($uuid, $user, $fileUser);
13✔
820
                $this->throwIfUserIsNotSigner($user, $fileUser);
9✔
821
                $this->throwIfAlreadySigned($fileEntity, $fileUser);
8✔
822
                $this->throwIfInvalidUser($uuid, $user);
7✔
823
                $userFolder = $this->root->getUserFolder($fileEntity->getUserId());
6✔
824
                $fileToSign = $userFolder->getById($fileEntity->getNodeId());
6✔
825
                if (count($fileToSign) < 1) {
6✔
826
                        throw new LibresignException(json_encode([
1✔
827
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
828
                                'errors' => [$this->l10n->t('File not found')],
1✔
829
                        ]));
1✔
830
                }
831
                /** @var File */
832
                $fileToSign = $fileToSign[0];
5✔
833
                $return = $this->getFileData($fileEntity, $user, $fileUser);
5✔
834
                $return['sign']['pdf'] = $this->getFileUrl($formatOfPdfOnSign, $fileEntity, $fileToSign, $uuid);
5✔
835
                return $return;
5✔
836
        }
837

838
        public function getInfoOfFileToSignUsingFileUuid(?string $uuid, ?IUser $user, string $formatOfPdfOnSign): array {
839
                $return = [];
×
840
                if (!$uuid) {
×
841
                        return $return;
×
842
                }
843
                try {
844
                        $fileEntity = $this->fileMapper->getByUuid($uuid);
×
845
                        $this->accountFileMapper->getByFileId($fileEntity->getId());
×
846
                } catch (DoesNotExistException $e) {
×
847
                        throw new LibresignException(json_encode([
×
848
                                'action' => JSActions::ACTION_DO_NOTHING,
×
849
                                'errors' => [$this->l10n->t('Invalid UUID')],
×
850
                        ]));
×
851
                }
852
                $this->throwIfAlreadySigned($fileEntity);
×
853
                try {
854
                        $this->validateHelper->userCanApproveValidationDocuments($user);
×
855
                } catch (LibresignException $e) {
×
856
                        throw new LibresignException(json_encode([
×
857
                                'action' => JSActions::ACTION_DO_NOTHING,
×
858
                                'errors' => [$e->getMessage()],
×
859
                        ]));
×
860
                }
861
                $userFolder = $this->root->getUserFolder($fileEntity->getUserId());
×
862
                $fileToSign = $userFolder->getById($fileEntity->getNodeId());
×
863
                if (count($fileToSign) < 1) {
×
864
                        throw new LibresignException(json_encode([
×
865
                                'action' => JSActions::ACTION_DO_NOTHING,
×
866
                                'errors' => [$this->l10n->t('File not found')],
×
867
                        ]));
×
868
                }
869
                /** @var File */
870
                $fileToSign = $fileToSign[0];
×
871
                $return = $this->getFileData($fileEntity, $user);
×
872
                $return['sign']['pdf'] = $this->getFileUrl($formatOfPdfOnSign, $fileEntity, $fileToSign, $uuid);
×
873
                return $return;
×
874
        }
875

876
        private function throwIfInvalidUser(string $uuid, ?IUser $user): void {
877
                if (!$user) {
7✔
878
                        throw new LibresignException(json_encode([
1✔
879
                                'action' => JSActions::ACTION_REDIRECT,
1✔
880
                                'errors' => [$this->l10n->t('You are not logged in. Please log in.')],
1✔
881
                                'redirect' => $this->urlGenerator->linkToRoute('core.login.showLoginForm', [
1✔
882
                                        'redirect_url' => $this->urlGenerator->linkToRoute(
1✔
883
                                                'libresign.page.sign',
1✔
884
                                                ['uuid' => $uuid]
1✔
885
                                        ),
1✔
886
                                ]),
1✔
887
                        ]));
1✔
888
                }
889
        }
890

891
        private function throwIfAlreadySigned(FileEntity $fileEntity, ?FileUserEntity $fileUser = null): void {
892
                if ($fileEntity->getStatus() === FileEntity::STATUS_SIGNED
8✔
893
                        || (!is_null($fileUser) && $fileUser->getSigned())
8✔
894
                ) {
895
                        throw new LibresignException(json_encode([
1✔
896
                                'action' => JSActions::ACTION_SHOW_ERROR,
1✔
897
                                'errors' => [$this->l10n->t('File already signed.')],
1✔
898
                                'uuid' => $fileEntity->getUuid(),
1✔
899
                        ]));
1✔
900
                }
901
        }
902

903
        private function trhowIfFileUserNotExists(string $uuid, ?IUser $user, ?FileUserEntity $fileUser): void {
904
                if ($fileUser instanceof FileUserEntity) {
13✔
905
                        $fileUserId = $fileUser->getUserId();
13✔
906
                        if ($fileUserId) {
13✔
907
                                return;
9✔
908
                        }
909
                }
910
                if ($user) {
4✔
911
                        throw new LibresignException(json_encode([
2✔
912
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
913
                                'errors' => [$this->l10n->t('This is not your file')],
2✔
914
                        ]));
2✔
915
                }
916
                $email = $fileUser->getEmail();
2✔
917
                if ($this->userManager->getByEmail($email)) {
2✔
918
                        throw new LibresignException(json_encode([
1✔
919
                                'action' => JSActions::ACTION_REDIRECT,
1✔
920
                                'errors' => [$this->l10n->t('User already exists. Please login.')],
1✔
921
                                'redirect' => $this->urlGenerator->linkToRoute('core.login.showLoginForm', [
1✔
922
                                        'redirect_url' => $this->urlGenerator->linkToRoute(
1✔
923
                                                'libresign.page.sign',
1✔
924
                                                ['uuid' => $uuid]
1✔
925
                                        ),
1✔
926
                                ]),
1✔
927
                        ]));
1✔
928
                }
929
                throw new LibresignException(json_encode([
1✔
930
                        'action' => JSActions::ACTION_CREATE_USER,
1✔
931
                        'settings' => ['accountHash' => md5($email)],
1✔
932
                ]));
1✔
933
        }
934

935
        private function throwIfUserIsNotSigner(?IUser $user, FileUserEntity $fileUser): void {
936
                if ($user && $fileUser->getUserId() !== $user->getUID()) {
9✔
937
                        throw new LibresignException(json_encode([
1✔
938
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
939
                                'errors' => [$this->l10n->t('Invalid user')],
1✔
940
                        ]));
1✔
941
                }
942
        }
943

944
        private function getFileData(FileEntity $fileData, IUser $user, ?FileUserEntity $fileUser = null): array {
945
                $return['action'] = JSActions::ACTION_SIGN;
5✔
946
                $return['sign'] = [
5✔
947
                        'uuid' => $fileData->getUuid(),
5✔
948
                        'filename' => $fileData->getName()
5✔
949
                ];
5✔
950
                if ($fileUser) {
5✔
951
                        $return['user']['name'] = $fileUser->getDisplayName();
5✔
952
                        $return['sign']['description'] = $fileUser->getDescription();
5✔
953
                } else {
954
                        $return['user']['name'] = $user->getDisplayName();
×
955
                }
956
                return $return;
5✔
957
        }
958

959
        /**
960
         * @return array
961
         *
962
         * @psalm-return array{file?: File, nodeId?: int, url?: string, base64?: string}
963
         */
964
        private function getFileUrl(string $format, FileEntity $fileEntity, File $fileToSign, string $uuid): array {
965
                $url = [];
5✔
966
                switch ($format) {
967
                        case 'base64':
5✔
968
                                $url = ['base64' => base64_encode($fileToSign->getContent())];
1✔
969
                                break;
1✔
970
                        case 'url':
4✔
971
                                try {
972
                                        $this->accountFileMapper->getByFileId($fileEntity->getId());
1✔
973
                                        $url = ['url' => $this->urlGenerator->linkToRoute('libresign.page.getPdf', ['uuid' => $uuid])];
1✔
974
                                } catch (DoesNotExistException $e) {
×
975
                                        $url = ['url' => $this->urlGenerator->linkToRoute('libresign.page.getPdfUser', ['uuid' => $uuid])];
×
976
                                }
977
                                break;
1✔
978
                        case 'nodeId':
3✔
979
                                $url = ['nodeId' => $fileToSign->getId()];
1✔
980
                                break;
1✔
981
                        case 'file':
2✔
982
                                $url = ['file' => $fileToSign];
2✔
983
                                break;
2✔
984
                }
985
                return $url;
5✔
986
        }
987
}
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

© 2025 Coveralls, Inc