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

LibreSign / libresign / 19878986893

03 Dec 2025 01:16AM UTC coverage: 41.576%. First build
19878986893

Pull #4464

github

web-flow
Merge 3ee0f466e into da0973853
Pull Request #4464: refactor: attach document

117 of 302 new or added lines in 16 files covered. (38.74%)

5123 of 12322 relevant lines covered (41.58%)

4.17 hits per line

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

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

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

146
        private function elementNeedFile(array $data): bool {
147
                return in_array($data['type'], ['signature', 'initial']);
×
148
        }
149

150
        private function getTypeOfFile(int $type): string {
151
                if ($type === self::TYPE_TO_SIGN) {
11✔
152
                        return $this->l10n->t('document to sign');
10✔
153
                }
154
                return $this->l10n->t('visible element');
1✔
155
        }
156

157
        public function validateBase64(string $base64, int $type = self::TYPE_TO_SIGN): void {
158
                $withMime = explode(',', $base64);
11✔
159
                if (count($withMime) === 2) {
11✔
160
                        $withMime[0] = explode(';', $withMime[0]);
4✔
161
                        if (count($withMime[0]) !== 2) {
4✔
162
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
×
163
                        }
164
                        if ($withMime[0][1] !== 'base64') {
4✔
165
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
1✔
166
                        }
167

168
                        if ($type === self::TYPE_TO_SIGN) {
3✔
169
                                if ($withMime[0][0] !== 'data:application/pdf') {
3✔
170
                                        throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
1✔
171
                                }
172
                        }
173
                        $base64 = $withMime[1];
2✔
174
                }
175
                $string = base64_decode($base64);
9✔
176
                if (in_array($type, [self::TYPE_VISIBLE_ELEMENT_USER, self::TYPE_VISIBLE_ELEMENT_PDF])) {
9✔
177
                        if (strlen($string) > 5000 * 1024) { // 5Mb
×
178
                                // TRANSLATORS Error when the visible element to add to document, like a signature or initial is bigger than normal
179
                                throw new InvalidArgumentException($this->l10n->t('File is too big'));
×
180
                        }
181
                }
182
                $newBase64 = base64_encode($string);
9✔
183
                if ($newBase64 !== $base64) {
9✔
184
                        throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
3✔
185
                }
186

187
                $mimeType = $this->mimeTypeDetector->detectString($string);
6✔
188

189
                if ($type === self::TYPE_TO_SIGN) {
6✔
190
                        if ($mimeType !== 'application/pdf') {
5✔
191
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
×
192
                        }
193
                } elseif ($mimeType !== 'image/png') {
1✔
194
                        if (in_array($type, [self::TYPE_VISIBLE_ELEMENT_USER, self::TYPE_VISIBLE_ELEMENT_PDF])) {
1✔
195
                                throw new LibresignException($this->l10n->t('File type: %s. Invalid Base64 file.', [$this->getTypeOfFile($type)]));
×
196
                        }
197
                }
198
        }
199

200
        public function validateNotRequestedSign(int $nodeId): void {
201
                try {
202
                        $fileMapper = $this->signRequestMapper->getByNodeId($nodeId);
3✔
203
                } catch (\Throwable) {
1✔
204
                }
205
                if (!empty($fileMapper)) {
3✔
206
                        throw new LibresignException($this->l10n->t('Already asked to sign this document'));
1✔
207
                }
208
        }
209

210
        public function validateVisibleElements(?array $visibleElements, int $type): void {
211
                if (!is_array($visibleElements)) {
2✔
212
                        throw new LibresignException($this->l10n->t('Visible elements need to be an array'));
1✔
213
                }
214
                foreach ($visibleElements as $element) {
1✔
215
                        $this->validateVisibleElement($element, $type);
1✔
216
                }
217
        }
218

219
        public function validateVisibleElement(array $element, int $type): void {
220
                $this->validateElementType($element);
3✔
221
                $this->validateElementSignRequestId($element, $type);
3✔
222
                $this->validateFile($element, $type);
3✔
223
                $this->validateElementCoordinates($element);
3✔
224
        }
225

226
        public function validateElementSignRequestId(array $element, int $type): void {
227
                if ($type !== self::TYPE_VISIBLE_ELEMENT_PDF) {
3✔
228
                        return;
1✔
229
                }
230
                if (!array_key_exists('signRequestId', $element)) {
2✔
231
                        // 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.
232
                        throw new LibresignException($this->l10n->t('Element must be associated with a user'));
×
233
                }
234
                try {
235
                        $this->signRequestMapper->getById($element['signRequestId']);
2✔
236
                } catch (\Throwable) {
×
237
                        throw new LibresignException($this->l10n->t('User not found for element.'));
×
238
                }
239
        }
240

241
        public function validateElementCoordinates(array $element): void {
242
                if (!array_key_exists('coordinates', $element)) {
5✔
243
                        return;
2✔
244
                }
245
                $this->validateElementPage($element);
3✔
246
                $this->validateElementCoordinate($element);
3✔
247
        }
248

249
        private function validateElementCoordinate(array $element): void {
250
                foreach ($element['coordinates'] as $type => $value) {
3✔
251
                        if (in_array($type, ['llx', 'lly', 'urx', 'ury', 'width', 'height', 'left', 'top'])) {
3✔
252
                                if (!is_int($value)) {
2✔
253
                                        throw new LibresignException($this->l10n->t('Coordinate %s must be an integer', [$type]));
×
254
                                }
255
                                if ($value < 0) {
2✔
256
                                        // TRANSLATORS Is an error that occur when the visible element added to the PDF file have your position outside the page margin
257
                                        throw new LibresignException($this->l10n->t('Object outside the page margin'));
×
258
                                }
259
                        }
260
                }
261
        }
262

263
        public function validateElementPage(array $element): void {
264
                if (!array_key_exists('page', $element['coordinates'])) {
5✔
265
                        return;
×
266
                }
267
                if (!is_int($element['coordinates']['page'])) {
5✔
268
                        throw new LibresignException($this->l10n->t('Page number must be an integer'));
1✔
269
                }
270
                if ($element['coordinates']['page'] < 1) {
4✔
271
                        throw new LibresignException($this->l10n->t('Page must be equal to or greater than 1'));
1✔
272
                }
273
        }
274

275
        public function validateElementType(array $element): void {
276
                if (!array_key_exists('type', $element)) {
10✔
277
                        if (!array_key_exists('elementId', $element)) {
1✔
278
                                throw new LibresignException($this->l10n->t('Element needs a type'));
1✔
279
                        }
280
                        return;
×
281
                }
282
                if (!in_array($element['type'], ['signature', 'initial', 'date', 'datetime', 'text'])) {
9✔
283
                        throw new LibresignException($this->l10n->t('Invalid element type'));
1✔
284
                }
285
        }
286

287
        public function validateVisibleElementsRelation(array $list, SignRequest $signRequest, ?IUser $user): void {
288
                $canCreateSignature = $this->signerElementsService->canCreateSignature();
2✔
289
                foreach ($list as $elements) {
2✔
290
                        if (!array_key_exists('documentElementId', $elements)) {
×
291
                                throw new LibresignException($this->l10n->t('Field %s not found', ['documentElementId']));
×
292
                        }
293
                        if ($canCreateSignature && !array_key_exists('profileNodeId', $elements)) {
×
294
                                throw new LibresignException($this->l10n->t('Field %s not found', ['profileNodeId']));
×
295
                        }
296
                        $this->validateSignerIsOwnerOfPdfVisibleElement($elements['documentElementId'], $signRequest);
×
297
                        if ($canCreateSignature && $user instanceof IUser) {
×
298
                                try {
299
                                        $this->userElementMapper->findOne(['file_id' => $elements['profileNodeId'], 'user_id' => $user->getUID()]);
×
300
                                } catch (\Throwable) {
×
301
                                        throw new LibresignException($this->l10n->t('Field %s does not belong to user', $elements['profileNodeId']));
×
302
                                }
303
                        }
304
                }
305
                $this->validateUserHasNecessaryElements($signRequest, $user, $list);
2✔
306
        }
307

308
        private function validateUserHasNecessaryElements(SignRequest $signRequest, ?IUser $user, array $list = []): void {
309
                $fileElements = $this->fileElementMapper->getByFileIdAndSignRequestId($signRequest->getFileId(), $signRequest->getId());
2✔
310
                $total = array_filter($fileElements, function (FileElement $fileElement) use ($list, $user): bool {
2✔
311
                        $found = array_filter($list, fn ($item): bool => $item['documentElementId'] === $fileElement->getId());
×
312
                        if (!$found) {
×
313
                                if (!$this->signerElementsService->canCreateSignature()) {
×
314
                                        return true;
×
315
                                }
316
                                try {
317
                                        if (!$user instanceof IUser) {
×
318
                                                throw new \Exception();
×
319
                                        }
320
                                        $this->userElementMapper->findMany([
×
321
                                                'user_id' => $user->getUID(),
×
322
                                                'type' => $fileElement->getType(),
×
323
                                        ]);
×
324
                                        return true;
×
325
                                } catch (\Throwable) {
×
326
                                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
×
327
                                }
328
                        }
329
                        return true;
×
330
                });
2✔
331
                if (count($total) !== count($fileElements)) {
2✔
332
                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
×
333
                }
334
        }
335

336
        private function validateSignerIsOwnerOfPdfVisibleElement(int $documentElementId, SignRequest $signRequest): void {
337
                $documentElement = $this->fileElementMapper->getById($documentElementId);
×
338
                if ($documentElement->getSignRequestId() !== $signRequest->getId()) {
×
339
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
340
                }
341
        }
342

343
        public function validateAuthenticatedUserIsOwnerOfPdfVisibleElement(int $documentElementId, string $uid): void {
344
                try {
345
                        $documentElement = $this->fileElementMapper->getById($documentElementId);
1✔
346
                        $signRequest = $this->signRequestMapper->getById($documentElement->getSignRequestId());
1✔
347
                        $file = $this->fileMapper->getById($signRequest->getFileId());
1✔
348
                        if ($file->getUserId() !== $uid) {
1✔
349
                                throw new LibresignException($this->l10n->t('Field %s does not belong to user', (string)$documentElementId));
1✔
350
                        }
351
                } catch (\Throwable) {
×
352
                        ($signRequest->getFileId());
×
353
                        throw new LibresignException($this->l10n->t('Field %s does not belong to user', (string)$documentElementId));
×
354
                }
355
        }
356

357
        public function validateIdDocIsOwnedByUser(int $nodeId, string $uid): void {
358
                try {
NEW
359
                        $this->idDocsMapper->getByUserIdAndNodeId($uid, $nodeId);
×
360
                } catch (\Throwable) {
×
361
                        throw new LibresignException($this->l10n->t('This file is not yours'));
×
362
                }
363
        }
364

365
        public function fileCanBeSigned(File $file): void {
366
                $statusList = [
2✔
367
                        File::STATUS_ABLE_TO_SIGN,
2✔
368
                        File::STATUS_PARTIAL_SIGNED
2✔
369
                ];
2✔
370
                if (!in_array($file->getStatus(), $statusList)) {
2✔
371
                        $statusText = $this->fileMapper->getTextOfStatus($file->getStatus());
×
372
                        throw new LibresignException($this->l10n->t('This file cannot be signed. Invalid status: %s', $statusText));
×
373
                }
374
        }
375

376
        public function validateIfNodeIdExists(int $nodeId, string $userId = '', int $type = self::TYPE_TO_SIGN): void {
377
                if (!$userId) {
5✔
378
                        $libresignFile = $this->fileMapper->getByFileId($nodeId);
4✔
379
                        $userId = $libresignFile->getUserId();
4✔
380
                }
381
                try {
382
                        $file = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
5✔
383
                } catch (NoUserException) {
2✔
384
                        throw new LibresignException($this->l10n->t('User not found.'));
1✔
385
                } catch (NotPermittedException) {
1✔
386
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
387
                }
388
                if (!$file) {
3✔
389
                        throw new LibresignException($this->l10n->t('File type: %s. Invalid fileID.', [$this->getTypeOfFile($type)]));
1✔
390
                }
391
        }
392

393
        public function validateMimeTypeAcceptedByNodeId(int $nodeId, string $userId = '', int $type = self::TYPE_TO_SIGN): void {
394
                if (!$userId) {
5✔
395
                        $libresignFile = $this->fileMapper->getByFileId($nodeId);
4✔
396
                        $userId = $libresignFile->getUserId();
4✔
397
                }
398
                $file = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
5✔
399
                $this->validateMimeTypeAcceptedByMime($file->getMimeType(), $type);
5✔
400
        }
401

402
        public function validateMimeTypeAcceptedByMime(string $mimetype, int $type = self::TYPE_TO_SIGN): void {
403
                switch ($type) {
404
                        case self::TYPE_TO_SIGN:
405
                                if ($mimetype !== 'application/pdf') {
18✔
406
                                        throw new LibresignException($this->l10n->t('File type: %s. Must be a fileID of %s format.', [$this->getTypeOfFile($type), 'PDF']));
1✔
407
                                }
408
                                break;
17✔
409
                        case self::TYPE_VISIBLE_ELEMENT_PDF:
410
                        case self::TYPE_VISIBLE_ELEMENT_USER:
411
                                if ($mimetype !== 'image/png') {
2✔
412
                                        throw new LibresignException($this->l10n->t('File type: %s. Must be a fileID of %s format.', [$this->getTypeOfFile($type), 'png']));
1✔
413
                                }
414
                                break;
1✔
415
                }
416
        }
417

418
        public function validateLibreSignNodeId(int $nodeId): void {
419
                try {
420
                        $this->getLibreSignFileByNodeId($nodeId);
7✔
421
                } catch (\Throwable) {
2✔
422
                        throw new LibresignException($this->l10n->t('Invalid fileID'));
2✔
423
                }
424
        }
425

426
        private function getLibreSignFileByNodeId(int $nodeId): ?\OCP\Files\File {
427
                if (isset($this->file[$nodeId])) {
7✔
428
                        return $this->file[$nodeId];
×
429
                }
430
                $libresignFile = $this->fileMapper->getByFileId($nodeId);
7✔
431

432
                $userFolder = $this->root->getUserFolder($libresignFile->getUserId());
6✔
433
                $file = $userFolder->getFirstNodeById($nodeId);
6✔
434
                if ($file instanceof \OCP\Files\File) {
5✔
435
                        $this->file[$nodeId] = $file;
3✔
436
                        return $this->file[$nodeId];
3✔
437
                }
438
                return null;
2✔
439
        }
440

441
        public function canRequestSign(IUser $user): void {
442
                $authorized = $this->appConfig->getValueArray(Application::APP_ID, 'groups_request_sign', ['admin']);
15✔
443
                if (empty($authorized)) {
15✔
444
                        $authorized = ['admin'];
2✔
445
                }
446
                if (!is_array($authorized)) {
15✔
447
                        throw new LibresignException(
×
448
                                json_encode([
×
449
                                        'action' => JSActions::ACTION_DO_NOTHING,
×
450
                                        'errors' => [['message' => $this->l10n->t('You are not allowed to request signing')]],
×
451
                                ]),
×
452
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
453
                        );
×
454
                }
455
                $userGroups = $this->groupManager->getUserGroupIds($user);
15✔
456
                if (!array_intersect($userGroups, $authorized)) {
15✔
457
                        throw new LibresignException(
7✔
458
                                json_encode([
7✔
459
                                        'action' => JSActions::ACTION_DO_NOTHING,
7✔
460
                                        'errors' => [['message' => $this->l10n->t('You are not allowed to request signing')]],
7✔
461
                                ]),
7✔
462
                                Http::STATUS_UNPROCESSABLE_ENTITY,
7✔
463
                        );
7✔
464
                }
465
        }
466

467
        public function iRequestedSignThisFile(IUser $user, int $nodeId): void {
468
                $libresignFile = $this->fileMapper->getByFileId($nodeId);
10✔
469
                if ($libresignFile->getUserId() !== $user->getUID()) {
10✔
470
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
471
                }
472
        }
473

474
        public function validateFileStatus(array $data): void {
475
                if (array_key_exists('status', $data)) {
2✔
476
                        $validStatusList = [
2✔
477
                                File::STATUS_DRAFT,
2✔
478
                                File::STATUS_ABLE_TO_SIGN,
2✔
479
                                File::STATUS_DELETED
2✔
480
                        ];
2✔
481
                        if (!in_array($data['status'], $validStatusList)) {
2✔
482
                                throw new LibresignException($this->l10n->t('Invalid status code for file.'));
×
483
                        }
484
                        if (!empty($data['uuid'])) {
2✔
485
                                $file = $this->fileMapper->getByUuid($data['uuid']);
1✔
486
                        } elseif (!empty($data['file']['fileId'])) {
1✔
487
                                try {
488
                                        $file = $this->fileMapper->getByFileId($data['file']['fileId']);
×
489
                                } catch (\Throwable) {
×
490
                                }
491
                        }
492
                        if (isset($file)) {
2✔
493
                                if ($data['status'] > $file->getStatus()) {
1✔
494
                                        if ($file->getStatus() >= File::STATUS_ABLE_TO_SIGN) {
×
495
                                                if ($data['status'] !== File::STATUS_DELETED) {
×
496
                                                        throw new LibresignException($this->l10n->t('Sign process already started. Unable to change status.'));
×
497
                                                }
498
                                        }
499
                                }
500
                        } elseif ($data['status'] === File::STATUS_DELETED) {
1✔
501
                                throw new LibresignException($this->l10n->t('Invalid status code for file.'));
×
502
                        }
503
                }
504
        }
505

506
        public function validateIdentifySigners(array $data): void {
507
                $this->validateSignersDataStructure($data);
22✔
508

509
                foreach ($data['users'] as $signer) {
18✔
510
                        $this->validateSignerData($signer);
18✔
511
                }
512
        }
513

514
        private function validateSignersDataStructure(array $data): void {
515
                if (empty($data) || !array_key_exists('users', $data) || !is_array($data['users']) || empty($data['users'])) {
22✔
516
                        throw new LibresignException($this->l10n->t('No signers'));
4✔
517
                }
518
        }
519

520
        private function validateSignerData(mixed $signer): void {
521
                if (!is_array($signer) || empty($signer)) {
18✔
522
                        throw new LibresignException($this->l10n->t('No signers'));
2✔
523
                }
524

525
                $this->validateSignerDisplayName($signer);
16✔
526
                $this->validateSignerIdentifyMethods($signer);
14✔
527
        }
528

529
        private function validateSignerDisplayName(array $signer): void {
530
                if (isset($signer['displayName']) && strlen($signer['displayName']) > 64) {
16✔
531
                        // It's an api error, don't translate
532
                        throw new LibresignException('Display name must not be longer than 64 characters');
2✔
533
                }
534
        }
535

536
        private function validateSignerIdentifyMethods(array $signer): void {
537
                $normalizedMethods = $this->normalizeIdentifyMethods($signer);
14✔
538

539
                foreach ($normalizedMethods as $method) {
9✔
540
                        $this->validateIdentifyMethodForRequest($method['name'], $method['value']);
9✔
541
                }
542
        }
543

544
        /**
545
         * @todo unify the key to be only 'identify' or only 'identifyMethods'
546
         */
547
        private function normalizeIdentifyMethods(array $signer): array {
548
                $key = array_key_exists('identifyMethods', $signer) ? 'identifyMethods' : 'identify';
14✔
549

550
                if (empty($signer[$key]) || !is_array($signer[$key])) {
14✔
551
                        throw new LibresignException('No identify methods for signer');
3✔
552
                }
553

554
                $normalizedMethods = [];
11✔
555

556
                foreach ($signer[$key] as $name => $data) {
11✔
557
                        $normalizedMethods[] = $this->normalizeIdentifyMethodEntry($key, $name, $data);
11✔
558
                }
559
                return $normalizedMethods;
9✔
560
        }
561

562
        /**
563
         * Extracted from normalizeIdentifyMethods to reduce cyclomatic complexity.
564
         */
565
        private function normalizeIdentifyMethodEntry(string $key, $name, $data): array {
566
                if ($key === 'identifyMethods') {
11✔
567
                        return $this->normalizeIdentifyMethodsStructure($data);
5✔
568
                } else {
569
                        return $this->normalizeIdentifyStructure($name, $data);
7✔
570
                }
571
        }
572

573
        private function normalizeIdentifyMethodsStructure(mixed $data): array {
574
                if (!is_array($data) || !array_key_exists('method', $data) || !array_key_exists('value', $data)) {
5✔
575
                        throw new LibresignException('Invalid identify method structure');
2✔
576
                }
577

578
                return [
3✔
579
                        'name' => $data['method'],
3✔
580
                        'value' => $data['value'],
3✔
581
                ];
3✔
582
        }
583

584
        private function normalizeIdentifyStructure(string $name, mixed $value): array {
585
                return [
7✔
586
                        'name' => $name,
7✔
587
                        'value' => $value,
7✔
588
                ];
7✔
589
        }
590

591
        private function validateIdentifyMethodForRequest(string $name, string $identifyValue): void {
592
                $identifyMethod = $this->identifyMethodService->getInstanceOfIdentifyMethod($name, $identifyValue);
11✔
593
                $identifyMethod->validateToRequest();
11✔
594

595
                $signatureMethods = $identifyMethod->getSignatureMethods();
11✔
596
                if (empty($signatureMethods)) {
11✔
597
                        // It's an api error, don't translate
598
                        throw new LibresignException('No signature methods for identify method ' . $name);
1✔
599
                }
600
        }
601

602
        public function validateExistingFile(array $data): void {
603
                if (isset($data['uuid'])) {
11✔
604
                        $this->validateFileUuid($data);
5✔
605
                        $file = $this->fileMapper->getByUuid($data['uuid']);
5✔
606
                        $this->iRequestedSignThisFile($data['userManager'], $file->getNodeId());
5✔
607
                } elseif (isset($data['file'])) {
6✔
608
                        if (!isset($data['file']['fileId'])) {
5✔
609
                                throw new LibresignException($this->l10n->t('Invalid fileID'));
1✔
610
                        }
611
                        $this->validateLibreSignNodeId($data['file']['fileId']);
4✔
612
                        $this->iRequestedSignThisFile($data['userManager'], $data['file']['fileId']);
3✔
613
                } else {
614
                        // TRANSLATORS This message is at API side. When an application or a
615
                        // developer send a structure to API without an UUID or without a
616
                        // File Object, throws this error. Normally LibreSign don't throws
617
                        // this error because the User Interface of LibreSign or send an
618
                        // UUID or a File object to API.
619
                        throw new LibresignException($this->l10n->t('Please provide either UUID or File object'));
1✔
620
                }
621
        }
622

623
        public function haveValidMail(array $data, ?int $type = null): void {
624
                if ($type === self::TYPE_TO_SIGN) {
4✔
625
                        return;
×
626
                }
627
                if (empty($data)) {
4✔
628
                        throw new LibresignException($this->l10n->t('No user data'));
1✔
629
                }
630
                if (empty($data['email'])) {
3✔
631
                        if (!empty($data['uid'])) {
1✔
632
                                $user = $this->userManager->get($data['uid']);
×
633
                                if (!$user) {
×
634
                                        throw new LibresignException($this->l10n->t('User not found.'));
×
635
                                }
636
                                if (!$user->getEMailAddress()) {
×
637
                                        // TRANSLATORS There is no email address for given user
638
                                        throw new LibresignException($this->l10n->t('User %s has no email address.', [$data['uid']]));
×
639
                                }
640
                        } else {
641
                                throw new LibresignException($this->l10n->t('Email required'));
1✔
642
                        }
643
                } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
2✔
644
                        throw new LibresignException($this->l10n->t('Invalid email'));
1✔
645
                }
646
        }
647

648
        public function signerWasAssociated(array $signer): void {
649
                try {
650
                        $libresignFile = $this->fileMapper->getByFileId();
3✔
651
                } catch (\Throwable) {
1✔
652
                        throw new LibresignException($this->l10n->t('File not loaded'));
1✔
653
                }
654
                $signatures = $this->signRequestMapper->getByFileUuid($libresignFile->getUuid());
2✔
655
                $exists = array_filter($signatures, function (SignRequest $signRequest) use ($signer): bool {
2✔
656
                        $key = key($signer);
1✔
657
                        $value = current($signer);
1✔
658
                        $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId());
1✔
659
                        $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($key, $value) {
1✔
660
                                if ($identifyMethod->getIdentifierKey() === $key && $identifyMethod->getIdentifierValue() === $value) {
1✔
661
                                        return true;
1✔
662
                                }
663
                                return false;
×
664
                        });
1✔
665
                        return count($found) > 0;
1✔
666
                });
2✔
667
                if (!$exists) {
2✔
668
                        throw new LibresignException($this->l10n->t('No signature was requested to %s', $signer['email']));
1✔
669
                }
670
        }
671

672
        public function notSigned(array $signer): void {
673
                try {
674
                        $libresignFile = $this->fileMapper->getByFileId();
2✔
675
                } catch (\Throwable) {
1✔
676
                        throw new LibresignException($this->l10n->t('File not loaded'));
1✔
677
                }
678
                $signatures = $this->signRequestMapper->getByFileUuid($libresignFile->getUuid());
1✔
679

680
                $exists = array_filter($signatures, function (SignRequest $signRequest) use ($signer): bool {
1✔
681
                        $key = key($signer);
1✔
682
                        $value = current($signer);
1✔
683
                        $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId());
1✔
684
                        $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($key, $value) {
1✔
685
                                if ($identifyMethod->getIdentifierKey() === $key && $identifyMethod->getIdentifierValue() === $value) {
1✔
686
                                        return true;
1✔
687
                                }
688
                                return false;
×
689
                        });
1✔
690
                        if (count($found) > 0) {
1✔
691
                                return $signRequest->getSigned() !== null;
1✔
692
                        }
693
                        return false;
×
694
                });
1✔
695
                if (!$exists) {
1✔
696
                        return;
1✔
697
                }
698
                $firstSigner = array_values($exists)[0];
×
699
                throw new LibresignException($this->l10n->t('%s already signed this file', $firstSigner->getDisplayName()));
×
700
        }
701

702
        public function validateFileUuid(array $data): void {
703
                try {
704
                        $this->fileMapper->getByUuid($data['uuid']);
7✔
705
                } catch (\Throwable) {
1✔
706
                        throw new LibresignException($this->l10n->t('Invalid UUID file'));
1✔
707
                }
708
        }
709

710
        public function validateSigner(string $uuid, ?IUser $user = null): void {
711
                $this->validateSignerUuidExists($uuid);
5✔
712
                $this->validateIdentifyMethod($uuid, $user);
4✔
713
        }
714

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

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

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

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

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

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

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

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

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

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

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

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

819
                if (!empty($methodsList)) {
2✔
820
                        return $this->getFirstAvailableMethod($methodsList);
2✔
821
                }
822

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

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

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

850
                return null;
2✔
851
        }
852

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

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

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

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

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

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