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

LibreSign / libresign / 21268273482

22 Jan 2026 11:13PM UTC coverage: 45.185%. First build
21268273482

Pull #6526

github

web-flow
Merge 16c32ce29 into 068f2ad0f
Pull Request #6526: fix: progress cache factory

84 of 183 new or added lines in 6 files covered. (45.9%)

7348 of 16262 relevant lines covered (45.19%)

4.95 hits per line

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

61.64
/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\IdentifyMethodMapper;
22
use OCA\Libresign\Db\SignRequest;
23
use OCA\Libresign\Db\SignRequestMapper;
24
use OCA\Libresign\Db\UserElementMapper;
25
use OCA\Libresign\Enum\FileStatus;
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
        public const VALID_MIMETIPE = [
52
                'application/pdf',
53
                'image/png',
54
        ];
55

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

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

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

162
        private function elementNeedFile(array $data): bool {
163
                return in_array($data['type'], ['signature', 'initial']);
×
164
        }
165

166
        private function getTypeOfFile(int $type): string {
167
                if ($type === self::TYPE_TO_SIGN) {
12✔
168
                        return $this->l10n->t('document to sign');
11✔
169
                }
170
                return $this->l10n->t('visible element');
1✔
171
        }
172

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

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

203
                $mimeType = $this->mimeTypeDetector->detectString($string);
5✔
204

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

216
        public function validateNotRequestedSign(int $nodeId): void {
217
                try {
218
                        $fileMapper = $this->signRequestMapper->getByNodeId($nodeId);
3✔
219
                } catch (\Throwable) {
1✔
220
                }
221
                if (!empty($fileMapper)) {
3✔
222
                        throw new LibresignException($this->l10n->t('Already asked to sign this document'));
1✔
223
                }
224
        }
225

226
        public function validateVisibleElements(?array $visibleElements, int $type): void {
227
                if (!is_array($visibleElements)) {
2✔
228
                        throw new LibresignException($this->l10n->t('Visible elements need to be an array'));
1✔
229
                }
230
                foreach ($visibleElements as $element) {
1✔
231
                        $this->validateVisibleElement($element, $type);
1✔
232
                }
233
        }
234

235
        public function validateVisibleElement(array $element, int $type): void {
236
                $this->validateElementType($element);
3✔
237
                $this->validateElementSignRequestId($element, $type);
3✔
238
                $this->validateFile($element, $type);
3✔
239
                $this->validateElementCoordinates($element);
3✔
240
        }
241

242
        public function validateElementSignRequestId(array $element, int $type): void {
243
                if ($type !== self::TYPE_VISIBLE_ELEMENT_PDF) {
3✔
244
                        return;
1✔
245
                }
246
                if (!array_key_exists('signRequestId', $element) && !array_key_exists('uuid', $element)) {
2✔
247
                        // 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.
248
                        throw new LibresignException($this->l10n->t('Element must be associated with a user'));
×
249
                }
250

251
                $getter = array_key_exists('signRequestId', $element)
2✔
252
                        ? fn () => $this->signRequestMapper->getById($element['signRequestId'])
2✔
253
                        : fn () => $this->signRequestMapper->getByUuid($element['uuid']);
2✔
254

255
                try {
256
                        $getter();
2✔
257
                } catch (\Throwable) {
×
258
                        throw new LibresignException($this->l10n->t('User not found for element.'));
×
259
                }
260
        }
261

262
        public function validateElementCoordinates(array $element): void {
263
                if (!array_key_exists('coordinates', $element)) {
5✔
264
                        return;
2✔
265
                }
266
                $this->validateElementPage($element);
3✔
267
                $this->validateElementCoordinate($element);
3✔
268
        }
269

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

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

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

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

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

357
        private function validateSignerIsOwnerOfPdfVisibleElement(int $documentElementId, SignRequest $signRequest): void {
358
                $documentElement = $this->fileElementMapper->getById($documentElementId);
×
359
                if ($documentElement->getSignRequestId() !== $signRequest->getId()) {
×
360
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
361
                }
362
        }
363

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

378
        public function validateIdDocIsOwnedByUser(int $nodeId, string $uid): void {
379
                try {
380
                        $this->idDocsMapper->getByUserIdAndNodeId($uid, $nodeId);
×
381
                } catch (\Throwable) {
×
382
                        throw new LibresignException($this->l10n->t('This file is not yours'));
×
383
                }
384
        }
385

386
        public function fileCanBeSigned(File $file): void {
387
                $statusList = [
2✔
388
                        FileStatus::ABLE_TO_SIGN->value,
2✔
389
                        FileStatus::PARTIAL_SIGNED->value
2✔
390
                ];
2✔
391
                if (!in_array($file->getStatus(), $statusList)) {
2✔
392
                        $statusText = $this->fileMapper->getTextOfStatus($file->getStatus());
×
393
                        throw new LibresignException($this->l10n->t('This file cannot be signed. Invalid status: %s', $statusText));
×
394
                }
395
        }
396

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

414
        public function validateMimeTypeAcceptedByNodeId(int $nodeId, string $userId = '', int $type = self::TYPE_TO_SIGN): void {
415
                if (!$userId) {
7✔
416
                        $libresignFile = $this->fileMapper->getByNodeId($nodeId);
5✔
417
                        $userId = $libresignFile->getUserId();
5✔
418
                }
419
                $file = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
7✔
420
                $this->validateMimeTypeAcceptedByMime($file->getMimeType(), $type);
7✔
421
        }
422

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

439
        public function validateLibreSignFileId(int $fileId): void {
440
                try {
441
                        $this->fileMapper->getById($fileId);
5✔
442
                } catch (\Throwable) {
1✔
443
                        throw new LibresignException($this->l10n->t('Invalid fileID'));
1✔
444
                }
445
        }
446

447
        private function getLibreSignFileByNodeId(int $nodeId): ?\OCP\Files\File {
448
                if (isset($this->file[$nodeId])) {
×
449
                        return $this->file[$nodeId];
×
450
                }
451
                $libresignFile = $this->fileMapper->getByNodeId($nodeId);
×
452

453
                $userFolder = $this->root->getUserFolder($libresignFile->getUserId());
×
454
                $file = $userFolder->getFirstNodeById($nodeId);
×
455
                if ($file instanceof \OCP\Files\File) {
×
456
                        $this->file[$nodeId] = $file;
×
457
                        return $this->file[$nodeId];
×
458
                }
459
                return null;
×
460
        }
461

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

488
        public function iRequestedSignThisFile(IUser $user, int $fileId): void {
489
                $libresignFile = $this->fileMapper->getById($fileId);
10✔
490
                if ($libresignFile->getUserId() !== $user->getUID()) {
10✔
491
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
492
                }
493
        }
494

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

527
        public function validateIdentifySigners(array $data): void {
528
                if (empty($data['users'])) {
22✔
529
                        return;
3✔
530
                }
531

532
                $this->validateSignersDataStructure($data);
19✔
533

534
                foreach ($data['users'] as $signer) {
18✔
535
                        $this->validateSignerData($signer);
18✔
536
                }
537
        }
538

539
        private function validateSignersDataStructure(array $data): void {
540
                if (empty($data) || !array_key_exists('users', $data) || !is_array($data['users']) || empty($data['users'])) {
19✔
541
                        throw new LibresignException($this->l10n->t('No signers'));
1✔
542
                }
543
        }
544

545
        private function validateSignerData(mixed $signer): void {
546
                if (!is_array($signer) || empty($signer)) {
18✔
547
                        throw new LibresignException($this->l10n->t('No signers'));
2✔
548
                }
549

550
                $this->validateSignerDisplayName($signer);
16✔
551
                $this->validateSignerIdentifyMethods($signer);
14✔
552
        }
553

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

561
        private function validateSignerIdentifyMethods(array $signer): void {
562
                $normalizedMethods = $this->normalizeIdentifyMethods($signer);
14✔
563

564
                foreach ($normalizedMethods as $method) {
9✔
565
                        $this->validateIdentifyMethodForRequest($method['name'], $method['value']);
9✔
566
                }
567
        }
568

569
        /**
570
         * @todo unify the key to be only 'identify' or only 'identifyMethods'
571
         */
572
        private function normalizeIdentifyMethods(array $signer): array {
573
                $key = array_key_exists('identifyMethods', $signer) ? 'identifyMethods' : 'identify';
14✔
574

575
                if (empty($signer[$key]) || !is_array($signer[$key])) {
14✔
576
                        throw new LibresignException('No identify methods for signer');
3✔
577
                }
578

579
                $normalizedMethods = [];
11✔
580

581
                foreach ($signer[$key] as $name => $data) {
11✔
582
                        $normalizedMethods[] = $this->normalizeIdentifyMethodEntry($key, $name, $data);
11✔
583
                }
584
                return $normalizedMethods;
9✔
585
        }
586

587
        /**
588
         * Extracted from normalizeIdentifyMethods to reduce cyclomatic complexity.
589
         */
590
        private function normalizeIdentifyMethodEntry(string $key, $name, $data): array {
591
                if ($key === 'identifyMethods') {
11✔
592
                        return $this->normalizeIdentifyMethodsStructure($data);
5✔
593
                } else {
594
                        return $this->normalizeIdentifyStructure($name, $data);
7✔
595
                }
596
        }
597

598
        private function normalizeIdentifyMethodsStructure(mixed $data): array {
599
                if (!is_array($data) || !array_key_exists('method', $data) || !array_key_exists('value', $data)) {
5✔
600
                        throw new LibresignException('Invalid identify method structure');
2✔
601
                }
602

603
                return [
3✔
604
                        'name' => $data['method'],
3✔
605
                        'value' => $data['value'],
3✔
606
                ];
3✔
607
        }
608

609
        private function normalizeIdentifyStructure(string $name, mixed $value): array {
610
                return [
7✔
611
                        'name' => $name,
7✔
612
                        'value' => $value,
7✔
613
                ];
7✔
614
        }
615

616
        private function validateIdentifyMethodForRequest(string $name, string $identifyValue): void {
617
                $identifyMethod = $this->identifyMethodService->getInstanceOfIdentifyMethod($name, $identifyValue);
11✔
618
                $identifyMethod->validateToRequest();
11✔
619

620
                $signatureMethods = $identifyMethod->getSignatureMethods();
11✔
621
                if (empty($signatureMethods)) {
11✔
622
                        // It's an api error, don't translate
623
                        throw new LibresignException('No signature methods for identify method ' . $name);
1✔
624
                }
625
        }
626

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

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

673
        public function validateFileUuid(array $data): void {
674
                try {
675
                        $this->fileMapper->getByUuid($data['uuid']);
7✔
676
                } catch (\Throwable) {
1✔
677
                        throw new LibresignException($this->l10n->t('Invalid UUID file'));
1✔
678
                }
679
        }
680

681
        public function validateSigner(string $uuid, ?IUser $user = null): void {
682
                $this->validateSignerUuidExists($uuid);
5✔
683
                $this->validateSignerStatus($uuid);
4✔
684
                $this->validateIdentifyMethod($uuid, $user);
4✔
685
        }
686

687
        public function validateSignerUuid(string $uuid): void {
NEW
688
                $this->validateSignerUuidExists($uuid);
×
689
        }
690

691
        /**
692
         * @throws LibresignException
693
         */
694
        private function validateSignerStatus(string $uuid): void {
695
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
4✔
696
                $status = $signRequest->getStatusEnum();
4✔
697

698
                if ($status === \OCA\Libresign\Enum\SignRequestStatus::DRAFT) {
4✔
699
                        throw new LibresignException(json_encode([
×
700
                                'action' => JSActions::ACTION_DO_NOTHING,
×
701
                                'errors' => [['message' => $this->l10n->t('You are not allowed to sign this document yet')]],
×
702
                        ]));
×
703
                }
704

705
                if ($status === \OCA\Libresign\Enum\SignRequestStatus::SIGNED) {
4✔
706
                        throw new LibresignException(json_encode([
×
707
                                'action' => JSActions::ACTION_DO_NOTHING,
×
708
                                'errors' => [['message' => $this->l10n->t('Document already signed')]],
×
709
                        ]));
×
710
                }
711
        }
712

713
        public function validateRenewSigner(string $uuid, ?IUser $user = null): void {
714
                $this->validateSignerUuidExists($uuid);
×
715
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
×
716
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
717
                foreach ($identifyMethods as $methods) {
×
718
                        foreach ($methods as $identifyMethod) {
×
719
                                $identifyMethod->validateToRenew($user);
×
720
                        }
721
                }
722
        }
723

724
        private function validateIdentifyMethod(string $uuid, ?IUser $user = null): void {
725
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
4✔
726
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
4✔
727
                foreach ($identifyMethods as $methods) {
4✔
728
                        foreach ($methods as $identifyMethod) {
4✔
729
                                $identifyMethod->validateToIdentify();
4✔
730
                        }
731
                }
732
        }
733

734
        private function validateSignerUuidExists(string $uuid): void {
735
                $this->validateUuidFormat($uuid);
5✔
736
                try {
737
                        $signRequest = $this->signRequestMapper->getByUuid($uuid);
4✔
738
                        $this->fileMapper->getById($signRequest->getFileId());
4✔
739
                } catch (DoesNotExistException) {
×
740
                        throw new LibresignException(json_encode([
×
741
                                'action' => JSActions::ACTION_DO_NOTHING,
×
742
                                'errors' => [['message' => $this->l10n->t('Invalid UUID')]],
×
743
                        ]));
×
744
                }
745
        }
746

747
        /**
748
         * @throws LibresignException
749
         */
750
        public function validateUuidFormat(string $uuid): void {
751
                if (!$uuid || !preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', $uuid)) {
7✔
752
                        throw new LibresignException(json_encode([
2✔
753
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
754
                                'errors' => [['message' => $this->l10n->t('Invalid UUID')]],
2✔
755
                        ]), Http::STATUS_NOT_FOUND);
2✔
756
                }
757
        }
758

759
        public function validateIsSignerOfFile(int $signRequestId, int $fileId): void {
760
                try {
761
                        $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
3✔
762
                } catch (\Throwable) {
1✔
763
                        throw new LibresignException($this->l10n->t('Signer not associated to this file'));
1✔
764
                }
765
        }
766

767
        public function validateUserHasNoFileWithThisType(string $uid, string $type): void {
768
                $exists = $this->idDocsMapper->getByUserAndType($uid, $type);
3✔
769
                if ($exists !== null) {
3✔
770
                        throw new LibresignException($this->l10n->t('A file of this type has been associated.'));
1✔
771
                }
772
        }
773

774
        public function canSignWithIdentificationDocumentStatus(?IUser $user, int $status): void {
775
                if ($user && $this->userCanApproveValidationDocuments($user, false)) {
3✔
776
                        return;
×
777
                }
778

779
                $allowedStatus = [
3✔
780
                        FileService::IDENTIFICATION_DOCUMENTS_DISABLED,
3✔
781
                        FileService::IDENTIFICATION_DOCUMENTS_APPROVED,
3✔
782
                ];
3✔
783
                if (!in_array($status, $allowedStatus)) {
3✔
784
                        throw new LibresignException($this->l10n->t('You need to have an approved identification document to sign.'));
×
785
                }
786
        }
787

788
        public function validateCredentials(SignRequest $signRequest, string $identifyMethodName, string $identifyValue, string $token): void {
789
                if ($identifyMethodName === IdentifyMethodService::IDENTIFY_PASSWORD && $token === '') {
2✔
790
                        throw new LibresignException($this->l10n->t('libresign', 'Invalid password'));
2✔
791
                }
792
                $this->validateIfIdentifyMethodExists($identifyMethodName);
×
793
                if ($signRequest->getSigned()) {
×
794
                        throw new LibresignException($this->l10n->t('File already signed.'));
×
795
                }
796
                $identifyMethod = $this->resolveIdentifyMethod($signRequest, $identifyMethodName, $identifyValue);
×
797
                $identifyMethod->setCodeSentByUser($token);
×
798
                $identifyMethod->validateToSign();
×
799
        }
800

801
        private function resolveIdentifyMethod(SignRequest $signRequest, string $methodName, ?string $identifyValue): IIdentifyMethod {
802
                if (!$signRequest->getId()) {
×
803
                        return $this->identifyMethodService
×
804
                                ->setCurrentIdentifyMethod()
×
805
                                ->getInstanceOfIdentifyMethod($methodName, $identifyValue);
×
806
                }
807

808
                $methodsList = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
809
                $identifyMethod = $this->searchMethodByNameAndValue($methodsList, $methodName, $identifyValue);
×
810
                if ($identifyMethod) {
×
811
                        return $identifyMethod;
×
812
                }
813

814
                $signMethods = $this->identifyMethodService->getSignMethodsOfIdentifiedFactors($signRequest->getId());
×
815
                $identifyMethod = $this->searchMethodByNameAndValue($signMethods, $methodName, $identifyValue);
×
816
                if ($identifyMethod) {
×
817
                        return $identifyMethod;
×
818
                }
819

820
                if (!empty($methodsList)) {
×
821
                        return $this->getFirstAvailableMethod($methodsList);
×
822
                }
823

824
                if (!empty($signMethods)) {
×
825
                        return $this->getFirstAvailableMethod($signMethods);
×
826
                }
827

828
                throw new LibresignException($this->l10n->t('Invalid identification method'));
×
829
        }
830

831
        private function searchMethodByNameAndValue(array $methods, string $methodName, ?string $identifyValue): ?IIdentifyMethod {
832
                if (isset($methods[$methodName])) {
×
833
                        if ($identifyValue) {
×
834
                                foreach ($methods[$methodName] as $identifyMethod) {
×
835
                                        if (!$identifyMethod instanceof IIdentifyMethod) {
×
836
                                                $identifyMethod = $this->getIdentifyMethodByNameAndValue($methodName, $identifyValue);
×
837
                                        }
838
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $identifyValue) {
×
839
                                                return $identifyMethod;
×
840
                                        }
841
                                }
842
                        } else {
843
                                $identifyMethod = current($methods[$methodName]);
×
844
                                if (!$identifyMethod instanceof IIdentifyMethod) {
×
845
                                        $identifyMethod = $this->getIdentifyMethodByNameAndValue($methodName, $identifyValue);
×
846
                                }
847
                                return $identifyMethod;
×
848
                        }
849
                }
850

851
                return null;
×
852
        }
853

854
        private function getIdentifyMethodByNameAndValue(string $identifyMethodName, ?string $identifyValue): IIdentifyMethod {
855
                return $this->identifyMethodService
×
856
                        ->setCurrentIdentifyMethod()
×
857
                        ->getInstanceOfIdentifyMethod($identifyMethodName, $identifyValue);
×
858
        }
859

860
        private function getFirstAvailableMethod(array $methods): IIdentifyMethod {
861
                foreach ($methods as $methodGroup) {
×
862
                        if (!empty($methodGroup)) {
×
863
                                return current($methodGroup);
×
864
                        }
865
                }
866
                throw new LibresignException($this->l10n->t('Invalid identification method'));
×
867
        }
868

869
        public function validateIfIdentifyMethodExists(string $identifyMethod): void {
870
                if (!in_array($identifyMethod, IdentifyMethodService::IDENTIFY_METHODS)) {
8✔
871
                        // TRANSLATORS When is requested to a person to sign a file, is
872
                        // necessary identify what is the identification method. The
873
                        // identification method is used to define how will be the sign
874
                        // flow.
875
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
2✔
876
                }
877
        }
878

879
        public function validateFileTypeExists(string $type): void {
880
                $profileFileTypes = $this->fileTypeMapper->getTypes();
3✔
881
                if (!array_key_exists($type, $profileFileTypes)) {
3✔
882
                        throw new LibresignException($this->l10n->t('Invalid file type.'));
1✔
883
                }
884
        }
885

886
        public function userCanApproveValidationDocuments(?IUser $user, bool $throw = true): bool {
887
                if ($user == null) {
4✔
888
                        return false;
×
889
                }
890

891
                $authorized = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
4✔
892
                if (!$authorized || !is_array($authorized) || empty($authorized)) {
4✔
893
                        $authorized = ['admin'];
×
894
                }
895
                $userGroups = $this->groupManager->getUserGroupIds($user);
4✔
896
                if (!array_intersect($userGroups, $authorized)) {
4✔
897
                        if ($throw) {
3✔
898
                                throw new LibresignException($this->l10n->t('You are not allowed to approve user profile documents.'));
×
899
                        }
900
                        return false;
3✔
901
                }
902
                return true;
1✔
903
        }
904
}
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