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

LibreSign / libresign / 20734978640

06 Jan 2026 01:38AM UTC coverage: 44.807%. First build
20734978640

Pull #6335

github

web-flow
Merge 1c0740970 into 7e27d60b2
Pull Request #6335: feat: refactor file list service

160 of 232 new or added lines in 4 files covered. (68.97%)

6691 of 14933 relevant lines covered (44.81%)

5.02 hits per line

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

66.6
/lib/Helper/ValidateHelper.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Helper;
10

11
use InvalidArgumentException;
12
use OC\AppFramework\Http;
13
use OC\User\NoUserException;
14
use OCA\Libresign\AppInfo\Application;
15
use OCA\Libresign\Db\File;
16
use OCA\Libresign\Db\FileElement;
17
use OCA\Libresign\Db\FileElementMapper;
18
use OCA\Libresign\Db\FileMapper;
19
use OCA\Libresign\Db\FileTypeMapper;
20
use OCA\Libresign\Db\IdDocsMapper;
21
use OCA\Libresign\Db\IdentifyMethod;
22
use OCA\Libresign\Db\IdentifyMethodMapper;
23
use OCA\Libresign\Db\SignRequest;
24
use OCA\Libresign\Db\SignRequestMapper;
25
use OCA\Libresign\Db\UserElementMapper;
26
use OCA\Libresign\Exception\LibresignException;
27
use OCA\Libresign\Service\FileService;
28
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
29
use OCA\Libresign\Service\IdentifyMethodService;
30
use OCA\Libresign\Service\SignerElementsService;
31
use OCP\AppFramework\Db\DoesNotExistException;
32
use OCP\Files\IMimeTypeDetector;
33
use OCP\Files\IRootFolder;
34
use OCP\Files\NotFoundException;
35
use OCP\Files\NotPermittedException;
36
use OCP\IAppConfig;
37
use OCP\IGroupManager;
38
use OCP\IL10N;
39
use OCP\IUser;
40
use OCP\IUserManager;
41
use OCP\Security\IHasher;
42

43
class ValidateHelper {
44
        /** @var \OCP\Files\File[] */
45
        private $file = [];
46

47
        public const TYPE_TO_SIGN = 1;
48
        public const TYPE_VISIBLE_ELEMENT_PDF = 2;
49
        public const TYPE_VISIBLE_ELEMENT_USER = 3;
50
        public const TYPE_ACCOUNT_DOCUMENT = 4;
51

52
        public const STATUS_DRAFT = 0;
53
        public const STATUS_ABLE_TO_SIGN = 1;
54
        public const STATUS_PARTIAL_SIGNED = 2;
55
        public const STATUS_SIGNED = 3;
56
        public const STATUS_DELETED = 4;
57
        public const VALID_MIMETIPE = [
58
                'application/pdf',
59
                'image/png',
60
        ];
61

62
        public function __construct(
63
                private IL10N $l10n,
64
                private SignRequestMapper $signRequestMapper,
65
                private FileMapper $fileMapper,
66
                private FileTypeMapper $fileTypeMapper,
67
                private FileElementMapper $fileElementMapper,
68
                private IdDocsMapper $idDocsMapper,
69
                private UserElementMapper $userElementMapper,
70
                private IdentifyMethodMapper $identifyMethodMapper,
71
                private IdentifyMethodService $identifyMethodService,
72
                private SignerElementsService $signerElementsService,
73
                private IMimeTypeDetector $mimeTypeDetector,
74
                private IHasher $hasher,
75
                private IAppConfig $appConfig,
76
                private IGroupManager $groupManager,
77
                private IUserManager $userManager,
78
                private IRootFolder $root,
79
        ) {
80
        }
134✔
81

82
        public function validateNewFile(array $data, int $type = self::TYPE_TO_SIGN, ?IUser $user = null): void {
83
                $this->validateFile($data, $type, $user);
4✔
84
                if (!empty($data['file']['fileId'])) {
4✔
85
                        $this->validateNotRequestedSign((int)$data['file']['fileId']);
1✔
86
                } elseif (!empty($data['file']['path'])) {
3✔
87
                        $userFolder = $this->root->getUserFolder($user?->getUID() ?? $data['userManager']->getUID());
×
88
                        try {
89
                                $node = $userFolder->get($data['file']['path']);
×
90
                        } catch (NotFoundException) {
×
91
                                throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404);
×
92
                        }
93
                        $this->validateNotRequestedSign($node->getId());
×
94
                }
95
        }
96

97
        /**
98
         * @property array $data
99
         * @property int $type to_sign|visible_element
100
         */
101
        public function validateFile(array $data, int $type = self::TYPE_TO_SIGN, ?IUser $user = null): void {
102
                if (empty($data['file'])) {
13✔
103
                        if (!empty($data['files'])) {
3✔
NEW
104
                                foreach ($data['files'] as $fileItem) {
×
NEW
105
                                        $this->validateFile([
×
NEW
106
                                                'file' => $fileItem,
×
NEW
107
                                                'userManager' => $data['userManager'] ?? null,
×
NEW
108
                                                'type' => $data['type'] ?? null,
×
NEW
109
                                        ], $type, $user);
×
110
                                }
NEW
111
                                return;
×
112
                        }
113
                        if ($type === self::TYPE_TO_SIGN) {
3✔
114
                                throw new LibresignException($this->l10n->t('File type: %s. Empty file.', [$this->getTypeOfFile($type)]));
1✔
115
                        }
116
                        if ($type === self::TYPE_VISIBLE_ELEMENT_USER) {
2✔
117
                                if ($this->elementNeedFile($data)) {
×
118
                                        throw new LibresignException($this->l10n->t('Elements of type %s need file.', [$data['type']]));
×
119
                                }
120
                        }
121
                        return;
2✔
122
                }
123
                if (!empty($data['file']['url'])) {
10✔
124
                        if (!filter_var($data['file']['url'], FILTER_VALIDATE_URL)) {
×
125
                                throw new LibresignException($this->l10n->t('File type: %s. Specify a URL, a Base64 string or a fileID.', [$this->getTypeOfFile($type)]));
×
126
                        }
127
                } elseif (!empty($data['file']['fileId'])) {
10✔
128
                        if (!is_numeric($data['file']['fileId'])) {
2✔
129
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid fileID.', [$this->getTypeOfFile($type)]));
1✔
130
                        }
131
                        if (!is_a($user, IUser::class)) {
1✔
132
                                if (!is_a($data['userManager'], IUser::class)) {
1✔
133
                                        throw new LibresignException($this->l10n->t('User not found.'));
×
134
                                }
135
                        }
136
                        $this->validateIfNodeIdExists((int)$data['file']['fileId'], $data['userManager']->getUID(), $type);
1✔
137
                        $this->validateMimeTypeAcceptedByNodeId((int)$data['file']['fileId'], $data['userManager']->getUID(), $type);
1✔
138
                } elseif (!empty($data['file']['nodeId'])) {
8✔
139
                        if (!is_numeric($data['file']['nodeId'])) {
3✔
140
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid fileID.', [$this->getTypeOfFile($type)]));
1✔
141
                        }
142
                        if (!is_a($user, IUser::class)) {
2✔
143
                                if (!isset($data['userManager']) || !is_a($data['userManager'], IUser::class)) {
2✔
144
                                        throw new LibresignException($this->l10n->t('User not found.'));
1✔
145
                                }
146
                        }
147
                        $this->validateIfNodeIdExists((int)$data['file']['nodeId'], $data['userManager']->getUID(), $type);
1✔
148
                        $this->validateMimeTypeAcceptedByNodeId((int)$data['file']['nodeId'], $data['userManager']->getUID(), $type);
1✔
149
                } elseif (!empty($data['file']['base64'])) {
5✔
150
                        $this->validateBase64($data['file']['base64'], $type);
4✔
151
                } elseif (!empty($data['file']['path'])) {
1✔
152
                        if (!is_a($user, IUser::class)) {
×
153
                                if (!is_a($data['userManager'], IUser::class)) {
×
154
                                        throw new LibresignException($this->l10n->t('User not found.'));
×
155
                                }
156
                        }
157
                        $userFolder = $this->root->getUserFolder($user?->getUID() ?? $data['userManager']->getUID());
×
158
                        try {
159
                                $userFolder->get($data['file']['path']);
×
160
                        } catch (NotFoundException) {
×
161
                                throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404);
×
162
                        }
163
                } else {
164
                        throw new LibresignException($this->l10n->t('File type: %s. Specify a URL, Base64 string, path or a fileID.', [$this->getTypeOfFile($type)]));
1✔
165
                }
166
        }
167

168
        private function elementNeedFile(array $data): bool {
169
                return in_array($data['type'], ['signature', 'initial']);
×
170
        }
171

172
        private function getTypeOfFile(int $type): string {
173
                if ($type === self::TYPE_TO_SIGN) {
12✔
174
                        return $this->l10n->t('document to sign');
11✔
175
                }
176
                return $this->l10n->t('visible element');
1✔
177
        }
178

179
        public function validateBase64(string $base64, int $type = self::TYPE_TO_SIGN): void {
180
                $withMime = explode(',', $base64);
10✔
181
                if (count($withMime) === 2) {
10✔
182
                        $withMime[0] = explode(';', $withMime[0]);
4✔
183
                        if (count($withMime[0]) !== 2) {
4✔
184
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
×
185
                        }
186
                        if ($withMime[0][1] !== 'base64') {
4✔
187
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
1✔
188
                        }
189

190
                        if ($type === self::TYPE_TO_SIGN) {
3✔
191
                                if ($withMime[0][0] !== 'data:application/pdf') {
3✔
192
                                        throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
1✔
193
                                }
194
                        }
195
                        $base64 = $withMime[1];
2✔
196
                }
197
                $string = base64_decode($base64);
8✔
198
                if (in_array($type, [self::TYPE_VISIBLE_ELEMENT_USER, self::TYPE_VISIBLE_ELEMENT_PDF])) {
8✔
199
                        if (strlen($string) > 5000 * 1024) { // 5Mb
×
200
                                // TRANSLATORS Error when the visible element to add to document, like a signature or initial is bigger than normal
201
                                throw new InvalidArgumentException($this->l10n->t('File is too big'));
×
202
                        }
203
                }
204
                $newBase64 = base64_encode($string);
8✔
205
                if ($newBase64 !== $base64) {
8✔
206
                        throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
3✔
207
                }
208

209
                $mimeType = $this->mimeTypeDetector->detectString($string);
5✔
210

211
                if ($type === self::TYPE_TO_SIGN) {
5✔
212
                        if ($mimeType !== 'application/pdf') {
4✔
213
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
×
214
                        }
215
                } elseif ($mimeType !== 'image/png') {
1✔
216
                        if (in_array($type, [self::TYPE_VISIBLE_ELEMENT_USER, self::TYPE_VISIBLE_ELEMENT_PDF])) {
1✔
217
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
×
218
                        }
219
                }
220
        }
221

222
        public function validateNotRequestedSign(int $nodeId): void {
223
                try {
224
                        $fileMapper = $this->signRequestMapper->getByNodeId($nodeId);
3✔
225
                } catch (\Throwable) {
1✔
226
                }
227
                if (!empty($fileMapper)) {
3✔
228
                        throw new LibresignException($this->l10n->t('Already asked to sign this document'));
1✔
229
                }
230
        }
231

232
        public function validateVisibleElements(?array $visibleElements, int $type): void {
233
                if (!is_array($visibleElements)) {
2✔
234
                        throw new LibresignException($this->l10n->t('Visible elements need to be an array'));
1✔
235
                }
236
                foreach ($visibleElements as $element) {
1✔
237
                        $this->validateVisibleElement($element, $type);
1✔
238
                }
239
        }
240

241
        public function validateVisibleElement(array $element, int $type): void {
242
                $this->validateElementType($element);
3✔
243
                $this->validateElementSignRequestId($element, $type);
3✔
244
                $this->validateFile($element, $type);
3✔
245
                $this->validateElementCoordinates($element);
3✔
246
        }
247

248
        public function validateElementSignRequestId(array $element, int $type): void {
249
                if ($type !== self::TYPE_VISIBLE_ELEMENT_PDF) {
3✔
250
                        return;
1✔
251
                }
252
                if (!array_key_exists('signRequestId', $element) && !array_key_exists('uuid', $element)) {
2✔
253
                        // TRANSLATION The element can be an image or text. It has to be associated with an user. The element will be added to the document.
254
                        throw new LibresignException($this->l10n->t('Element must be associated with a user'));
×
255
                }
256

257
                $getter = array_key_exists('signRequestId', $element)
2✔
258
                        ? fn () => $this->signRequestMapper->getById($element['signRequestId'])
2✔
259
                        : fn () => $this->signRequestMapper->getByUuid($element['uuid']);
2✔
260

261
                try {
262
                        $getter();
2✔
263
                } catch (\Throwable) {
×
264
                        throw new LibresignException($this->l10n->t('User not found for element.'));
×
265
                }
266
        }
267

268
        public function validateElementCoordinates(array $element): void {
269
                if (!array_key_exists('coordinates', $element)) {
5✔
270
                        return;
2✔
271
                }
272
                $this->validateElementPage($element);
3✔
273
                $this->validateElementCoordinate($element);
3✔
274
        }
275

276
        private function validateElementCoordinate(array $element): void {
277
                foreach ($element['coordinates'] as $type => $value) {
3✔
278
                        if (in_array($type, ['llx', 'lly', 'urx', 'ury', 'width', 'height', 'left', 'top'])) {
3✔
279
                                if (!is_int($value)) {
2✔
280
                                        throw new LibresignException($this->l10n->t('Coordinate %s must be an integer', [$type]));
×
281
                                }
282
                                if ($value < 0) {
2✔
283
                                        // TRANSLATORS Is an error that occur when the visible element added to the PDF file have your position outside the page margin
284
                                        throw new LibresignException($this->l10n->t('Object outside the page margin'));
×
285
                                }
286
                        }
287
                }
288
        }
289

290
        public function validateElementPage(array $element): void {
291
                if (!array_key_exists('page', $element['coordinates'])) {
5✔
292
                        return;
×
293
                }
294
                if (!is_int($element['coordinates']['page'])) {
5✔
295
                        throw new LibresignException($this->l10n->t('Page number must be an integer'));
1✔
296
                }
297
                if ($element['coordinates']['page'] < 1) {
4✔
298
                        throw new LibresignException($this->l10n->t('Page must be equal to or greater than 1'));
1✔
299
                }
300
        }
301

302
        public function validateElementType(array $element): void {
303
                if (!array_key_exists('type', $element)) {
10✔
304
                        if (!array_key_exists('elementId', $element)) {
1✔
305
                                throw new LibresignException($this->l10n->t('Element needs a type'));
1✔
306
                        }
307
                        return;
×
308
                }
309
                if (!in_array($element['type'], ['signature', 'initial', 'date', 'datetime', 'text'])) {
9✔
310
                        throw new LibresignException($this->l10n->t('Invalid element type'));
1✔
311
                }
312
        }
313

314
        public function validateVisibleElementsRelation(array $list, SignRequest $signRequest, ?IUser $user): void {
315
                $canCreateSignature = $this->signerElementsService->canCreateSignature();
2✔
316
                foreach ($list as $elements) {
2✔
317
                        if (!array_key_exists('documentElementId', $elements)) {
×
318
                                throw new LibresignException($this->l10n->t('Field %s not found', ['documentElementId']));
×
319
                        }
320
                        if ($canCreateSignature && !array_key_exists('profileNodeId', $elements)) {
×
321
                                throw new LibresignException($this->l10n->t('Field %s not found', ['profileNodeId']));
×
322
                        }
323
                        $this->validateSignerIsOwnerOfPdfVisibleElement($elements['documentElementId'], $signRequest);
×
324
                        if ($canCreateSignature && $user instanceof IUser) {
×
325
                                try {
326
                                        $this->userElementMapper->findOne(['node_id' => $elements['profileNodeId'], 'user_id' => $user->getUID()]);
×
327
                                } catch (\Throwable) {
×
328
                                        throw new LibresignException($this->l10n->t('Field %s does not belong to user', $elements['profileNodeId']));
×
329
                                }
330
                        }
331
                }
332
                $this->validateUserHasNecessaryElements($signRequest, $user, $list);
2✔
333
        }
334

335
        private function validateUserHasNecessaryElements(SignRequest $signRequest, ?IUser $user, array $list = []): void {
336
                $fileElements = $this->fileElementMapper->getByFileIdAndSignRequestId($signRequest->getFileId(), $signRequest->getId());
2✔
337
                $total = array_filter($fileElements, function (FileElement $fileElement) use ($list, $user): bool {
2✔
338
                        $found = array_filter($list, fn ($item): bool => $item['documentElementId'] === $fileElement->getId());
×
339
                        if (!$found) {
×
340
                                if (!$this->signerElementsService->canCreateSignature()) {
×
341
                                        return true;
×
342
                                }
343
                                try {
344
                                        if (!$user instanceof IUser) {
×
345
                                                throw new \Exception();
×
346
                                        }
347
                                        $this->userElementMapper->findMany([
×
348
                                                'user_id' => $user->getUID(),
×
349
                                                'type' => $fileElement->getType(),
×
350
                                        ]);
×
351
                                        return true;
×
352
                                } catch (\Throwable) {
×
353
                                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
×
354
                                }
355
                        }
356
                        return true;
×
357
                });
2✔
358
                if (count($total) !== count($fileElements)) {
2✔
359
                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
×
360
                }
361
        }
362

363
        private function validateSignerIsOwnerOfPdfVisibleElement(int $documentElementId, SignRequest $signRequest): void {
364
                $documentElement = $this->fileElementMapper->getById($documentElementId);
×
365
                if ($documentElement->getSignRequestId() !== $signRequest->getId()) {
×
366
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
367
                }
368
        }
369

370
        public function validateAuthenticatedUserIsOwnerOfPdfVisibleElement(int $documentElementId, string $uid): void {
371
                try {
372
                        $documentElement = $this->fileElementMapper->getById($documentElementId);
1✔
373
                        $signRequest = $this->signRequestMapper->getById($documentElement->getSignRequestId());
1✔
374
                        $file = $this->fileMapper->getById($signRequest->getFileId());
1✔
375
                        if ($file->getUserId() !== $uid) {
1✔
376
                                throw new LibresignException($this->l10n->t('Field %s does not belong to user', (string)$documentElementId));
1✔
377
                        }
378
                } catch (\Throwable) {
×
379
                        ($signRequest->getFileId());
×
380
                        throw new LibresignException($this->l10n->t('Field %s does not belong to user', (string)$documentElementId));
×
381
                }
382
        }
383

384
        public function validateIdDocIsOwnedByUser(int $nodeId, string $uid): void {
385
                try {
386
                        $this->idDocsMapper->getByUserIdAndNodeId($uid, $nodeId);
×
387
                } catch (\Throwable) {
×
388
                        throw new LibresignException($this->l10n->t('This file is not yours'));
×
389
                }
390
        }
391

392
        public function fileCanBeSigned(File $file): void {
393
                $statusList = [
2✔
394
                        File::STATUS_ABLE_TO_SIGN,
2✔
395
                        File::STATUS_PARTIAL_SIGNED
2✔
396
                ];
2✔
397
                if (!in_array($file->getStatus(), $statusList)) {
2✔
398
                        $statusText = $this->fileMapper->getTextOfStatus($file->getStatus());
×
399
                        throw new LibresignException($this->l10n->t('This file cannot be signed. Invalid status: %s', $statusText));
×
400
                }
401
        }
402

403
        public function validateIfNodeIdExists(int $nodeId, string $userId = '', int $type = self::TYPE_TO_SIGN): void {
404
                if (!$userId) {
6✔
405
                        $libresignFile = $this->fileMapper->getByNodeId($nodeId);
4✔
406
                        $userId = $libresignFile->getUserId();
4✔
407
                }
408
                try {
409
                        $file = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
6✔
410
                } catch (NoUserException) {
2✔
411
                        throw new LibresignException($this->l10n->t('User not found.'));
1✔
412
                } catch (NotPermittedException) {
1✔
413
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
414
                }
415
                if (!$file) {
4✔
416
                        throw new LibresignException($this->l10n->t('File type: %s. Invalid fileID.', [$this->getTypeOfFile($type)]));
1✔
417
                }
418
        }
419

420
        public function validateMimeTypeAcceptedByNodeId(int $nodeId, string $userId = '', int $type = self::TYPE_TO_SIGN): void {
421
                if (!$userId) {
7✔
422
                        $libresignFile = $this->fileMapper->getByNodeId($nodeId);
5✔
423
                        $userId = $libresignFile->getUserId();
5✔
424
                }
425
                $file = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
7✔
426
                $this->validateMimeTypeAcceptedByMime($file->getMimeType(), $type);
7✔
427
        }
428

429
        public function validateMimeTypeAcceptedByMime(string $mimetype, int $type = self::TYPE_TO_SIGN): void {
430
                switch ($type) {
431
                        case self::TYPE_TO_SIGN:
432
                                if ($mimetype !== 'application/pdf') {
20✔
433
                                        throw new LibresignException($this->l10n->t('File type: %s. Must be a fileID of %s format.', [$this->getTypeOfFile($type), 'PDF']));
1✔
434
                                }
435
                                break;
19✔
436
                        case self::TYPE_VISIBLE_ELEMENT_PDF:
437
                        case self::TYPE_VISIBLE_ELEMENT_USER:
438
                                if ($mimetype !== 'image/png') {
2✔
439
                                        throw new LibresignException($this->l10n->t('File type: %s. Must be a fileID of %s format.', [$this->getTypeOfFile($type), 'png']));
1✔
440
                                }
441
                                break;
1✔
442
                }
443
        }
444

445
        public function validateLibreSignFileId(int $fileId): void {
446
                try {
447
                        $this->fileMapper->getById($fileId);
5✔
448
                } catch (\Throwable) {
1✔
449
                        throw new LibresignException($this->l10n->t('Invalid fileID'));
1✔
450
                }
451
        }
452

453
        private function getLibreSignFileByNodeId(int $nodeId): ?\OCP\Files\File {
454
                if (isset($this->file[$nodeId])) {
×
455
                        return $this->file[$nodeId];
×
456
                }
457
                $libresignFile = $this->fileMapper->getByNodeId($nodeId);
×
458

459
                $userFolder = $this->root->getUserFolder($libresignFile->getUserId());
×
460
                $file = $userFolder->getFirstNodeById($nodeId);
×
461
                if ($file instanceof \OCP\Files\File) {
×
462
                        $this->file[$nodeId] = $file;
×
463
                        return $this->file[$nodeId];
×
464
                }
465
                return null;
×
466
        }
467

468
        public function canRequestSign(IUser $user): void {
469
                $authorized = $this->appConfig->getValueArray(Application::APP_ID, 'groups_request_sign', ['admin']);
15✔
470
                if (empty($authorized)) {
15✔
471
                        $authorized = ['admin'];
2✔
472
                }
473
                if (!is_array($authorized)) {
15✔
474
                        throw new LibresignException(
×
475
                                json_encode([
×
476
                                        'action' => JSActions::ACTION_DO_NOTHING,
×
477
                                        'errors' => [['message' => $this->l10n->t('You are not allowed to request signing')]],
×
478
                                ]),
×
479
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
480
                        );
×
481
                }
482
                $userGroups = $this->groupManager->getUserGroupIds($user);
15✔
483
                if (!array_intersect($userGroups, $authorized)) {
15✔
484
                        throw new LibresignException(
7✔
485
                                json_encode([
7✔
486
                                        'action' => JSActions::ACTION_DO_NOTHING,
7✔
487
                                        'errors' => [['message' => $this->l10n->t('You are not allowed to request signing')]],
7✔
488
                                ]),
7✔
489
                                Http::STATUS_UNPROCESSABLE_ENTITY,
7✔
490
                        );
7✔
491
                }
492
        }
493

494
        public function iRequestedSignThisFile(IUser $user, int $fileId): void {
495
                $libresignFile = $this->fileMapper->getById($fileId);
10✔
496
                if ($libresignFile->getUserId() !== $user->getUID()) {
10✔
497
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
498
                }
499
        }
500

501
        public function validateFileStatus(array $data): void {
502
                if (array_key_exists('status', $data)) {
2✔
503
                        $validStatusList = [
2✔
504
                                File::STATUS_DRAFT,
2✔
505
                                File::STATUS_ABLE_TO_SIGN,
2✔
506
                                File::STATUS_DELETED
2✔
507
                        ];
2✔
508
                        if (!in_array($data['status'], $validStatusList)) {
2✔
509
                                throw new LibresignException($this->l10n->t('Invalid status code for file.'));
×
510
                        }
511
                        if (!empty($data['uuid'])) {
2✔
512
                                $file = $this->fileMapper->getByUuid($data['uuid']);
1✔
513
                        } elseif (!empty($data['file']['fileId'])) {
1✔
514
                                try {
515
                                        $file = $this->fileMapper->getById($data['file']['fileId']);
×
516
                                } catch (\Throwable) {
×
517
                                }
518
                        }
519
                        if (isset($file)) {
2✔
520
                                if ($data['status'] > $file->getStatus()) {
1✔
521
                                        if ($file->getStatus() >= File::STATUS_ABLE_TO_SIGN) {
×
522
                                                if ($data['status'] !== File::STATUS_DELETED) {
×
523
                                                        throw new LibresignException($this->l10n->t('Sign process already started. Unable to change status.'));
×
524
                                                }
525
                                        }
526
                                }
527
                        } elseif ($data['status'] === File::STATUS_DELETED) {
1✔
528
                                throw new LibresignException($this->l10n->t('Invalid status code for file.'));
×
529
                        }
530
                }
531
        }
532

533
        public function validateIdentifySigners(array $data): void {
534
                if (empty($data['users'])) {
22✔
535
                        return;
3✔
536
                }
537

538
                $this->validateSignersDataStructure($data);
19✔
539

540
                foreach ($data['users'] as $signer) {
18✔
541
                        $this->validateSignerData($signer);
18✔
542
                }
543
        }
544

545
        private function validateSignersDataStructure(array $data): void {
546
                if (empty($data) || !array_key_exists('users', $data) || !is_array($data['users']) || empty($data['users'])) {
19✔
547
                        throw new LibresignException($this->l10n->t('No signers'));
1✔
548
                }
549
        }
550

551
        private function validateSignerData(mixed $signer): void {
552
                if (!is_array($signer) || empty($signer)) {
18✔
553
                        throw new LibresignException($this->l10n->t('No signers'));
2✔
554
                }
555

556
                $this->validateSignerDisplayName($signer);
16✔
557
                $this->validateSignerIdentifyMethods($signer);
14✔
558
        }
559

560
        private function validateSignerDisplayName(array $signer): void {
561
                if (isset($signer['displayName']) && strlen($signer['displayName']) > 64) {
16✔
562
                        // It's an api error, don't translate
563
                        throw new LibresignException('Display name must not be longer than 64 characters');
2✔
564
                }
565
        }
566

567
        private function validateSignerIdentifyMethods(array $signer): void {
568
                $normalizedMethods = $this->normalizeIdentifyMethods($signer);
14✔
569

570
                foreach ($normalizedMethods as $method) {
9✔
571
                        $this->validateIdentifyMethodForRequest($method['name'], $method['value']);
9✔
572
                }
573
        }
574

575
        /**
576
         * @todo unify the key to be only 'identify' or only 'identifyMethods'
577
         */
578
        private function normalizeIdentifyMethods(array $signer): array {
579
                $key = array_key_exists('identifyMethods', $signer) ? 'identifyMethods' : 'identify';
14✔
580

581
                if (empty($signer[$key]) || !is_array($signer[$key])) {
14✔
582
                        throw new LibresignException('No identify methods for signer');
3✔
583
                }
584

585
                $normalizedMethods = [];
11✔
586

587
                foreach ($signer[$key] as $name => $data) {
11✔
588
                        $normalizedMethods[] = $this->normalizeIdentifyMethodEntry($key, $name, $data);
11✔
589
                }
590
                return $normalizedMethods;
9✔
591
        }
592

593
        /**
594
         * Extracted from normalizeIdentifyMethods to reduce cyclomatic complexity.
595
         */
596
        private function normalizeIdentifyMethodEntry(string $key, $name, $data): array {
597
                if ($key === 'identifyMethods') {
11✔
598
                        return $this->normalizeIdentifyMethodsStructure($data);
5✔
599
                } else {
600
                        return $this->normalizeIdentifyStructure($name, $data);
7✔
601
                }
602
        }
603

604
        private function normalizeIdentifyMethodsStructure(mixed $data): array {
605
                if (!is_array($data) || !array_key_exists('method', $data) || !array_key_exists('value', $data)) {
5✔
606
                        throw new LibresignException('Invalid identify method structure');
2✔
607
                }
608

609
                return [
3✔
610
                        'name' => $data['method'],
3✔
611
                        'value' => $data['value'],
3✔
612
                ];
3✔
613
        }
614

615
        private function normalizeIdentifyStructure(string $name, mixed $value): array {
616
                return [
7✔
617
                        'name' => $name,
7✔
618
                        'value' => $value,
7✔
619
                ];
7✔
620
        }
621

622
        private function validateIdentifyMethodForRequest(string $name, string $identifyValue): void {
623
                $identifyMethod = $this->identifyMethodService->getInstanceOfIdentifyMethod($name, $identifyValue);
11✔
624
                $identifyMethod->validateToRequest();
11✔
625

626
                $signatureMethods = $identifyMethod->getSignatureMethods();
11✔
627
                if (empty($signatureMethods)) {
11✔
628
                        // It's an api error, don't translate
629
                        throw new LibresignException('No signature methods for identify method ' . $name);
1✔
630
                }
631
        }
632

633
        public function validateExistingFile(array $data): void {
634
                if (isset($data['uuid'])) {
11✔
635
                        $this->validateFileUuid($data);
5✔
636
                        $file = $this->fileMapper->getByUuid($data['uuid']);
5✔
637
                        $this->iRequestedSignThisFile($data['userManager'], $file->getId());
5✔
638
                } elseif (isset($data['file'])) {
6✔
639
                        if (!isset($data['file']['fileId'])) {
5✔
640
                                throw new LibresignException($this->l10n->t('Invalid fileID'));
1✔
641
                        }
642
                        $this->validateLibreSignFileId($data['file']['fileId']);
4✔
643
                        $this->iRequestedSignThisFile($data['userManager'], $data['file']['fileId']);
3✔
644
                } else {
645
                        // TRANSLATORS This message is at API side. When an application or a
646
                        // developer send a structure to API without an UUID or without a
647
                        // File Object, throws this error. Normally LibreSign don't throws
648
                        // this error because the User Interface of LibreSign or send an
649
                        // UUID or a File object to API.
650
                        throw new LibresignException($this->l10n->t('Please provide either UUID or File object'));
1✔
651
                }
652
        }
653

654
        public function haveValidMail(array $data, ?int $type = null): void {
655
                if ($type === self::TYPE_TO_SIGN) {
4✔
656
                        return;
×
657
                }
658
                if (empty($data)) {
4✔
659
                        throw new LibresignException($this->l10n->t('No user data'));
1✔
660
                }
661
                if (empty($data['email'])) {
3✔
662
                        if (!empty($data['uid'])) {
1✔
663
                                $user = $this->userManager->get($data['uid']);
×
664
                                if (!$user) {
×
665
                                        throw new LibresignException($this->l10n->t('User not found.'));
×
666
                                }
667
                                if (!$user->getEMailAddress()) {
×
668
                                        // TRANSLATORS There is no email address for given user
669
                                        throw new LibresignException($this->l10n->t('User %s has no email address.', [$data['uid']]));
×
670
                                }
671
                        } else {
672
                                throw new LibresignException($this->l10n->t('Email required'));
1✔
673
                        }
674
                } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
2✔
675
                        throw new LibresignException($this->l10n->t('Invalid email'));
1✔
676
                }
677
        }
678

679
        public function signerWasAssociated(array $signer): void {
680
                try {
681
                        $libresignFile = $this->fileMapper->getByNodeId();
1✔
682
                } catch (\Throwable) {
×
683
                        throw new LibresignException($this->l10n->t('File not loaded'));
×
684
                }
685
                $signatures = $this->signRequestMapper->getByFileUuid($libresignFile->getUuid());
1✔
686
                $exists = array_filter($signatures, function (SignRequest $signRequest) use ($signer): bool {
1✔
687
                        $key = key($signer);
1✔
688
                        $value = current($signer);
1✔
689
                        $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId());
1✔
690
                        $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($key, $value) {
1✔
691
                                if ($identifyMethod->getIdentifierKey() === $key && $identifyMethod->getIdentifierValue() === $value) {
1✔
692
                                        return true;
1✔
693
                                }
694
                                return false;
×
695
                        });
1✔
696
                        return count($found) > 0;
1✔
697
                });
1✔
698
                if (!$exists) {
1✔
699
                        throw new LibresignException($this->l10n->t('No signature was requested to %s', $signer['email']));
×
700
                }
701
        }
702

703
        public function notSigned(array $signer): void {
704
                try {
705
                        $libresignFile = $this->fileMapper->getByNodeId();
1✔
706
                } catch (\Throwable) {
×
707
                        throw new LibresignException($this->l10n->t('File not loaded'));
×
708
                }
709
                $signatures = $this->signRequestMapper->getByFileUuid($libresignFile->getUuid());
1✔
710

711
                $exists = array_filter($signatures, function (SignRequest $signRequest) use ($signer): bool {
1✔
712
                        $key = key($signer);
1✔
713
                        $value = current($signer);
1✔
714
                        $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId());
1✔
715
                        $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($key, $value) {
1✔
716
                                if ($identifyMethod->getIdentifierKey() === $key && $identifyMethod->getIdentifierValue() === $value) {
1✔
717
                                        return true;
1✔
718
                                }
719
                                return false;
×
720
                        });
1✔
721
                        if (count($found) > 0) {
1✔
722
                                return $signRequest->getSigned() !== null;
1✔
723
                        }
724
                        return false;
×
725
                });
1✔
726
                if (!$exists) {
1✔
727
                        return;
1✔
728
                }
729
                $firstSigner = array_values($exists)[0];
×
730
                throw new LibresignException($this->l10n->t('%s already signed this file', $firstSigner->getDisplayName()));
×
731
        }
732

733
        public function validateFileUuid(array $data): void {
734
                try {
735
                        $this->fileMapper->getByUuid($data['uuid']);
7✔
736
                } catch (\Throwable) {
1✔
737
                        throw new LibresignException($this->l10n->t('Invalid UUID file'));
1✔
738
                }
739
        }
740

741
        public function validateSigner(string $uuid, ?IUser $user = null): void {
742
                $this->validateSignerUuidExists($uuid);
5✔
743
                $this->validateSignerStatus($uuid);
4✔
744
                $this->validateIdentifyMethod($uuid, $user);
4✔
745
        }
746

747
        /**
748
         * @throws LibresignException
749
         */
750
        private function validateSignerStatus(string $uuid): void {
751
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
4✔
752
                $status = $signRequest->getStatusEnum();
4✔
753

754
                if ($status === \OCA\Libresign\Enum\SignRequestStatus::DRAFT) {
4✔
755
                        throw new LibresignException(json_encode([
×
756
                                'action' => JSActions::ACTION_DO_NOTHING,
×
757
                                'errors' => [['message' => $this->l10n->t('You are not allowed to sign this document yet')]],
×
758
                        ]));
×
759
                }
760

761
                if ($status === \OCA\Libresign\Enum\SignRequestStatus::SIGNED) {
4✔
762
                        throw new LibresignException(json_encode([
×
763
                                'action' => JSActions::ACTION_DO_NOTHING,
×
764
                                'errors' => [['message' => $this->l10n->t('Document already signed')]],
×
765
                        ]));
×
766
                }
767
        }
768

769
        public function validateRenewSigner(string $uuid, ?IUser $user = null): void {
770
                $this->validateSignerUuidExists($uuid);
×
771
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
×
772
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
773
                foreach ($identifyMethods as $methods) {
×
774
                        foreach ($methods as $identifyMethod) {
×
775
                                $identifyMethod->validateToRenew($user);
×
776
                        }
777
                }
778
        }
779

780
        private function validateIdentifyMethod(string $uuid, ?IUser $user = null): void {
781
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
4✔
782
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
4✔
783
                foreach ($identifyMethods as $methods) {
4✔
784
                        foreach ($methods as $identifyMethod) {
4✔
785
                                $identifyMethod->validateToIdentify();
4✔
786
                        }
787
                }
788
        }
789

790
        private function validateSignerUuidExists(string $uuid): void {
791
                $this->validateUuidFormat($uuid);
5✔
792
                try {
793
                        $signRequest = $this->signRequestMapper->getByUuid($uuid);
4✔
794
                        $this->fileMapper->getById($signRequest->getFileId());
4✔
795
                } catch (DoesNotExistException) {
×
796
                        throw new LibresignException(json_encode([
×
797
                                'action' => JSActions::ACTION_DO_NOTHING,
×
798
                                'errors' => [['message' => $this->l10n->t('Invalid UUID')]],
×
799
                        ]));
×
800
                }
801
        }
802

803
        /**
804
         * @throws LibresignException
805
         */
806
        public function validateUuidFormat(string $uuid): void {
807
                if (!$uuid || !preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', $uuid)) {
7✔
808
                        throw new LibresignException(json_encode([
2✔
809
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
810
                                'errors' => [['message' => $this->l10n->t('Invalid UUID')]],
2✔
811
                        ]), Http::STATUS_NOT_FOUND);
2✔
812
                }
813
        }
814

815
        public function validateIsSignerOfFile(int $signRequestId, int $fileId): void {
816
                try {
817
                        $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
3✔
818
                } catch (\Throwable) {
1✔
819
                        throw new LibresignException($this->l10n->t('Signer not associated to this file'));
1✔
820
                }
821
        }
822

823
        public function validateUserHasNoFileWithThisType(string $uid, string $type): void {
824
                $exists = $this->idDocsMapper->getByUserAndType($uid, $type);
3✔
825
                if ($exists !== null) {
3✔
826
                        throw new LibresignException($this->l10n->t('A file of this type has been associated.'));
1✔
827
                }
828
        }
829

830
        public function canSignWithIdentificationDocumentStatus(?IUser $user, int $status): void {
831
                if ($user && $this->userCanApproveValidationDocuments($user, false)) {
3✔
832
                        return;
×
833
                }
834

835
                $allowedStatus = [
3✔
836
                        FileService::IDENTIFICATION_DOCUMENTS_DISABLED,
3✔
837
                        FileService::IDENTIFICATION_DOCUMENTS_APPROVED,
3✔
838
                ];
3✔
839
                if (!in_array($status, $allowedStatus)) {
3✔
840
                        throw new LibresignException($this->l10n->t('You need to have an approved identification document to sign.'));
×
841
                }
842
        }
843

844
        public function validateCredentials(SignRequest $signRequest, string $identifyMethodName, string $identifyValue, string $token): void {
845
                $this->validateIfIdentifyMethodExists($identifyMethodName);
2✔
846
                if ($signRequest->getSigned()) {
2✔
847
                        throw new LibresignException($this->l10n->t('File already signed.'));
×
848
                }
849
                $identifyMethod = $this->resolveIdentifyMethod($signRequest, $identifyMethodName, $identifyValue);
2✔
850
                $identifyMethod->setCodeSentByUser($token);
2✔
851
                $identifyMethod->validateToSign();
2✔
852
        }
853

854
        private function resolveIdentifyMethod(SignRequest $signRequest, string $methodName, ?string $identifyValue): IIdentifyMethod {
855
                if (!$signRequest->getId()) {
2✔
856
                        return $this->identifyMethodService
×
857
                                ->setCurrentIdentifyMethod()
×
858
                                ->getInstanceOfIdentifyMethod($methodName, $identifyValue);
×
859
                }
860

861
                $methodsList = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
2✔
862
                $identifyMethod = $this->searchMethodByNameAndValue($methodsList, $methodName, $identifyValue);
2✔
863
                if ($identifyMethod) {
2✔
864
                        return $identifyMethod;
×
865
                }
866

867
                $signMethods = $this->identifyMethodService->getSignMethodsOfIdentifiedFactors($signRequest->getId());
2✔
868
                $identifyMethod = $this->searchMethodByNameAndValue($signMethods, $methodName, $identifyValue);
2✔
869
                if ($identifyMethod) {
2✔
870
                        return $identifyMethod;
×
871
                }
872

873
                if (!empty($methodsList)) {
2✔
874
                        return $this->getFirstAvailableMethod($methodsList);
2✔
875
                }
876

877
                if (!empty($signMethods)) {
×
878
                        return $this->getFirstAvailableMethod($signMethods);
×
879
                }
880

881
                throw new LibresignException($this->l10n->t('Invalid identification method'));
×
882
        }
883

884
        private function searchMethodByNameAndValue(array $methods, string $methodName, ?string $identifyValue): ?IIdentifyMethod {
885
                if (isset($methods[$methodName])) {
2✔
886
                        if ($identifyValue) {
×
887
                                foreach ($methods[$methodName] as $identifyMethod) {
×
888
                                        if (!$identifyMethod instanceof IIdentifyMethod) {
×
889
                                                $identifyMethod = $this->getIdentifyMethodByNameAndValue($methodName, $identifyValue);
×
890
                                        }
891
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $identifyValue) {
×
892
                                                return $identifyMethod;
×
893
                                        }
894
                                }
895
                        } else {
896
                                $identifyMethod = current($methods[$methodName]);
×
897
                                if (!$identifyMethod instanceof IIdentifyMethod) {
×
898
                                        $identifyMethod = $this->getIdentifyMethodByNameAndValue($methodName, $identifyValue);
×
899
                                }
900
                                return $identifyMethod;
×
901
                        }
902
                }
903

904
                return null;
2✔
905
        }
906

907
        private function getIdentifyMethodByNameAndValue(string $identifyMethodName, ?string $identifyValue): IIdentifyMethod {
908
                return $this->identifyMethodService
×
909
                        ->setCurrentIdentifyMethod()
×
910
                        ->getInstanceOfIdentifyMethod($identifyMethodName, $identifyValue);
×
911
        }
912

913
        private function getFirstAvailableMethod(array $methods): IIdentifyMethod {
914
                foreach ($methods as $methodGroup) {
2✔
915
                        if (!empty($methodGroup)) {
2✔
916
                                return current($methodGroup);
2✔
917
                        }
918
                }
919
                throw new LibresignException($this->l10n->t('Invalid identification method'));
×
920
        }
921

922
        public function validateIfIdentifyMethodExists(string $identifyMethod): void {
923
                if (!in_array($identifyMethod, IdentifyMethodService::IDENTIFY_METHODS)) {
10✔
924
                        // TRANSLATORS When is requested to a person to sign a file, is
925
                        // necessary identify what is the identification method. The
926
                        // identification method is used to define how will be the sign
927
                        // flow.
928
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
2✔
929
                }
930
        }
931

932
        public function validateFileTypeExists(string $type): void {
933
                $profileFileTypes = $this->fileTypeMapper->getTypes();
3✔
934
                if (!array_key_exists($type, $profileFileTypes)) {
3✔
935
                        throw new LibresignException($this->l10n->t('Invalid file type.'));
1✔
936
                }
937
        }
938

939
        public function userCanApproveValidationDocuments(?IUser $user, bool $throw = true): bool {
940
                if ($user == null) {
4✔
941
                        return false;
×
942
                }
943

944
                $authorized = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
4✔
945
                if (!$authorized || !is_array($authorized) || empty($authorized)) {
4✔
946
                        $authorized = ['admin'];
×
947
                }
948
                $userGroups = $this->groupManager->getUserGroupIds($user);
4✔
949
                if (!array_intersect($userGroups, $authorized)) {
4✔
950
                        if ($throw) {
3✔
951
                                throw new LibresignException($this->l10n->t('You are not allowed to approve user profile documents.'));
×
952
                        }
953
                        return false;
3✔
954
                }
955
                return true;
1✔
956
        }
957
}
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