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

LibreSign / libresign / 22147773017

18 Feb 2026 04:16PM UTC coverage: 52.831%. First build
22147773017

Pull #6938

github

web-flow
Merge 312721399 into 725528158
Pull Request #6938: fix: docmdp first signature allow

59 of 68 new or added lines in 7 files covered. (86.76%)

9406 of 17804 relevant lines covered (52.83%)

6.27 hits per line

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

73.14
/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\DocMdp\Validator as DocMdpValidator;
28
use OCA\Libresign\Service\FileService;
29
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
30
use OCA\Libresign\Service\IdentifyMethodService;
31
use OCA\Libresign\Service\SequentialSigningService;
32
use OCA\Libresign\Service\SignerElementsService;
33
use OCP\AppFramework\Db\DoesNotExistException;
34
use OCP\Files\IMimeTypeDetector;
35
use OCP\Files\IRootFolder;
36
use OCP\Files\NotFoundException;
37
use OCP\Files\NotPermittedException;
38
use OCP\IAppConfig;
39
use OCP\IGroupManager;
40
use OCP\IL10N;
41
use OCP\IUser;
42
use OCP\IUserManager;
43
use OCP\Security\IHasher;
44

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

49
        public const TYPE_TO_SIGN = 1;
50
        public const TYPE_VISIBLE_ELEMENT_PDF = 2;
51
        public const TYPE_VISIBLE_ELEMENT_USER = 3;
52
        public const TYPE_ACCOUNT_DOCUMENT = 4;
53
        public const VALID_MIMETIPE = [
54
                'application/pdf',
55
                'image/png',
56
        ];
57

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

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

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

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

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

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

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

207
                $mimeType = $this->mimeTypeDetector->detectString($string);
5✔
208

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

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

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

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

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

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

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

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

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

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

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

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

335
        /**
336
         * @return SignRequest[]
337
         */
338
        private function getEnvelopeChildSignRequests(SignRequest $signRequest): array {
339
                $file = $this->fileMapper->getById($signRequest->getFileId());
8✔
340
                if (!$file->isEnvelope()) {
8✔
341
                        return [];
7✔
342
                }
343
                return $this->signRequestMapper->getByEnvelopeChildrenAndIdentifyMethod(
1✔
344
                        $file->getId(),
1✔
345
                        $signRequest->getId()
1✔
346
                );
1✔
347
        }
348

349
        /**
350
         * @param SignRequest[] $childSignRequests
351
         */
352
        private function validateUserHasNecessaryElements(
353
                SignRequest $signRequest,
354
                ?IUser $user,
355
                array $list = [],
356
                array $childSignRequests = [],
357
        ): void {
358
                $fileElements = $this->fileElementMapper->getByFileIdAndSignRequestId($signRequest->getFileId(), $signRequest->getId());
4✔
359
                if (empty($fileElements) && !empty($childSignRequests)) {
4✔
360
                        foreach ($childSignRequests as $childSr) {
1✔
361
                                $fileElements = array_merge(
1✔
362
                                        $fileElements,
1✔
363
                                        $this->fileElementMapper->getByFileIdAndSignRequestId($childSr->getFileId(), $childSr->getId())
1✔
364
                                );
1✔
365
                        }
366
                }
367
                $total = array_filter($fileElements, function (FileElement $fileElement) use ($list, $user): bool {
4✔
368
                        $found = array_filter($list, fn ($item): bool => $item['documentElementId'] === $fileElement->getId());
2✔
369
                        if (!$found) {
2✔
370
                                if (!$this->signerElementsService->canCreateSignature()) {
1✔
371
                                        return true;
×
372
                                }
373
                                try {
374
                                        if (!$user instanceof IUser) {
1✔
375
                                                throw new \Exception();
×
376
                                        }
377
                                        $this->userElementMapper->findMany([
1✔
378
                                                'user_id' => $user->getUID(),
1✔
379
                                                'type' => $fileElement->getType(),
1✔
380
                                        ]);
1✔
381
                                        return true;
×
382
                                } catch (\Throwable) {
1✔
383
                                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
1✔
384
                                }
385
                        }
386
                        return true;
1✔
387
                });
4✔
388
                if (count($total) !== count($fileElements)) {
3✔
389
                        throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
×
390
                }
391
        }
392

393
        /**
394
         * @param int[] $childSignRequestIds
395
         */
396
        private function validateSignerIsOwnerOfPdfVisibleElement(int $documentElementId, SignRequest $signRequest, array $childSignRequestIds = []): void {
397
                $documentElement = $this->fileElementMapper->getById($documentElementId);
3✔
398
                if ($documentElement->getSignRequestId() === $signRequest->getId()) {
3✔
399
                        return;
1✔
400
                }
401
                if (!empty($childSignRequestIds) && in_array($documentElement->getSignRequestId(), $childSignRequestIds, true)) {
2✔
402
                        return;
1✔
403
                }
404
                throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
1✔
405
        }
406

407
        public function validateAuthenticatedUserIsOwnerOfPdfVisibleElement(int $documentElementId, string $uid): void {
408
                try {
409
                        $documentElement = $this->fileElementMapper->getById($documentElementId);
3✔
410
                        $signRequest = $this->signRequestMapper->getById($documentElement->getSignRequestId());
3✔
411
                        $file = $this->fileMapper->getById($signRequest->getFileId());
3✔
412
                        if ($file->getUserId() !== $uid) {
3✔
413
                                throw new LibresignException($this->l10n->t('Field %s does not belong to user', (string)$documentElementId));
3✔
414
                        }
415
                } catch (\Throwable) {
1✔
416
                        ($signRequest->getFileId());
1✔
417
                        throw new LibresignException($this->l10n->t('Field %s does not belong to user', (string)$documentElementId));
1✔
418
                }
419
        }
420

421
        public function validateIdDocIsOwnedByUser(int $nodeId, string $uid): void {
422
                try {
423
                        $this->idDocsMapper->getByUserIdAndNodeId($uid, $nodeId);
×
424
                } catch (\Throwable) {
×
425
                        throw new LibresignException($this->l10n->t('This file is not yours'));
×
426
                }
427
        }
428

429
        public function validateIdDocBelongsToSignRequest(int $nodeId, int $signRequestId): void {
430
                try {
431
                        $this->idDocsMapper->getBySignRequestIdAndNodeId($signRequestId, $nodeId);
3✔
432
                } catch (\Throwable) {
2✔
433
                        throw new LibresignException($this->l10n->t('Not allowed'));
2✔
434
                }
435
        }
436

437
        public function fileCanBeSigned(File $file): void {
438
                $statusList = [
2✔
439
                        FileStatus::ABLE_TO_SIGN->value,
2✔
440
                        FileStatus::PARTIAL_SIGNED->value
2✔
441
                ];
2✔
442
                if (!in_array($file->getStatus(), $statusList)) {
2✔
443
                        $statusText = $this->fileMapper->getTextOfStatus($file->getStatus());
×
444
                        throw new LibresignException($this->l10n->t('This file cannot be signed. Invalid status: %s', $statusText));
×
445
                }
446
        }
447

448
        public function validateIfNodeIdExists(int $nodeId, string $userId = '', int $type = self::TYPE_TO_SIGN): void {
449
                if (!$userId) {
6✔
450
                        $libresignFile = $this->fileMapper->getByNodeId($nodeId);
4✔
451
                        $userId = $libresignFile->getUserId();
4✔
452
                }
453
                try {
454
                        $file = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
6✔
455
                } catch (NoUserException) {
2✔
456
                        throw new LibresignException($this->l10n->t('User not found.'));
1✔
457
                } catch (NotPermittedException) {
1✔
458
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
459
                }
460
                if (!$file) {
4✔
461
                        throw new LibresignException($this->l10n->t('File type: %s. Invalid fileID.', [$this->getTypeOfFile($type)]));
1✔
462
                }
463
        }
464

465
        public function validateMimeTypeAcceptedByNodeId(int $nodeId, string $userId = '', int $type = self::TYPE_TO_SIGN): void {
466
                if (!$userId) {
7✔
467
                        $libresignFile = $this->fileMapper->getByNodeId($nodeId);
5✔
468
                        $userId = $libresignFile->getUserId();
5✔
469
                }
470
                $file = $this->root->getUserFolder($userId)->getFirstNodeById($nodeId);
7✔
471
                $this->validateMimeTypeAcceptedByMime($file->getMimeType(), $type);
7✔
472
        }
473

474
        public function validateMimeTypeAcceptedByMime(string $mimetype, int $type = self::TYPE_TO_SIGN): void {
475
                switch ($type) {
476
                        case self::TYPE_TO_SIGN:
477
                                if ($mimetype !== 'application/pdf') {
20✔
478
                                        throw new LibresignException($this->l10n->t('File type: %s. Must be a fileID of %s format.', [$this->getTypeOfFile($type), 'PDF']));
1✔
479
                                }
480
                                break;
19✔
481
                        case self::TYPE_VISIBLE_ELEMENT_PDF:
482
                        case self::TYPE_VISIBLE_ELEMENT_USER:
483
                                if ($mimetype !== 'image/png') {
2✔
484
                                        throw new LibresignException($this->l10n->t('File type: %s. Must be a fileID of %s format.', [$this->getTypeOfFile($type), 'png']));
1✔
485
                                }
486
                                break;
1✔
487
                }
488
        }
489

490
        public function validateLibreSignFileId(int $fileId): void {
491
                try {
492
                        $this->fileMapper->getById($fileId);
5✔
493
                } catch (\Throwable) {
1✔
494
                        throw new LibresignException($this->l10n->t('Invalid fileID'));
1✔
495
                }
496
        }
497

498
        private function getLibreSignFileByNodeId(int $nodeId): ?\OCP\Files\File {
499
                if (isset($this->file[$nodeId])) {
×
500
                        return $this->file[$nodeId];
×
501
                }
502
                $libresignFile = $this->fileMapper->getByNodeId($nodeId);
×
503

504
                $userFolder = $this->root->getUserFolder($libresignFile->getUserId());
×
505
                $file = $userFolder->getFirstNodeById($nodeId);
×
506
                if ($file instanceof \OCP\Files\File) {
×
507
                        $this->file[$nodeId] = $file;
×
508
                        return $this->file[$nodeId];
×
509
                }
510
                return null;
×
511
        }
512

513
        public function canRequestSign(IUser $user): void {
514
                $authorized = $this->appConfig->getValueArray(Application::APP_ID, 'groups_request_sign', ['admin']);
15✔
515
                if (empty($authorized)) {
15✔
516
                        $authorized = ['admin'];
2✔
517
                }
518
                if (!is_array($authorized)) {
15✔
519
                        throw new LibresignException(
×
520
                                json_encode([
×
521
                                        'action' => JSActions::ACTION_DO_NOTHING,
×
522
                                        'errors' => [['message' => $this->l10n->t('You are not allowed to request signing')]],
×
523
                                ]),
×
524
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
525
                        );
×
526
                }
527
                $userGroups = $this->groupManager->getUserGroupIds($user);
15✔
528
                if (!array_intersect($userGroups, $authorized)) {
15✔
529
                        throw new LibresignException(
7✔
530
                                json_encode([
7✔
531
                                        'action' => JSActions::ACTION_DO_NOTHING,
7✔
532
                                        'errors' => [['message' => $this->l10n->t('You are not allowed to request signing')]],
7✔
533
                                ]),
7✔
534
                                Http::STATUS_UNPROCESSABLE_ENTITY,
7✔
535
                        );
7✔
536
                }
537
        }
538

539
        public function iRequestedSignThisFile(IUser $user, int $fileId): void {
540
                $libresignFile = $this->fileMapper->getById($fileId);
10✔
541
                if ($libresignFile->getUserId() !== $user->getUID()) {
10✔
542
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
543
                }
544
        }
545

546
        public function validateFileStatus(array $data): void {
547
                if (array_key_exists('status', $data)) {
2✔
548
                        $validStatusList = [
2✔
549
                                FileStatus::DRAFT->value,
2✔
550
                                FileStatus::ABLE_TO_SIGN->value,
2✔
551
                                FileStatus::DELETED->value
2✔
552
                        ];
2✔
553
                        if (!in_array($data['status'], $validStatusList)) {
2✔
554
                                throw new LibresignException($this->l10n->t('Invalid status code for file.'));
×
555
                        }
556
                        if (!empty($data['uuid'])) {
2✔
557
                                $file = $this->fileMapper->getByUuid($data['uuid']);
1✔
558
                        } elseif (!empty($data['file']['fileId'])) {
1✔
559
                                try {
560
                                        $file = $this->fileMapper->getById($data['file']['fileId']);
×
561
                                } catch (\Throwable) {
×
562
                                }
563
                        }
564
                        if (isset($file)) {
2✔
565
                                if ($data['status'] > $file->getStatus()) {
1✔
566
                                        if ($file->getStatus() >= FileStatus::ABLE_TO_SIGN->value) {
×
567
                                                if ($data['status'] !== FileStatus::DELETED->value) {
×
568
                                                        throw new LibresignException($this->l10n->t('Sign process already started. Unable to change status.'));
×
569
                                                }
570
                                        }
571
                                }
572
                        } elseif ($data['status'] === FileStatus::DELETED->value) {
1✔
573
                                throw new LibresignException($this->l10n->t('Invalid status code for file.'));
×
574
                        }
575
                }
576
        }
577

578
        public function validateIdentifySigners(array $data): void {
579
                if (empty($data['signers'])) {
25✔
580
                        return;
3✔
581
                }
582

583
                $this->validateSignersDataStructure($data);
22✔
584
                $this->docMdpValidator->validateSignersCount($data);
21✔
585
                $this->validateDocMdpPdfRestrictions($data);
21✔
586

587
                foreach ($data['signers'] as $signer) {
21✔
588
                        $this->validateSignerData($signer);
21✔
589
                }
590
        }
591

592
        private function validateSignersDataStructure(array $data): void {
593
                if (empty($data) || !array_key_exists('signers', $data) || !is_array($data['signers']) || empty($data['signers'])) {
22✔
594
                        throw new LibresignException($this->l10n->t('No signers'));
1✔
595
                }
596
        }
597

598
        private function validateSignerData(mixed $signer): void {
599
                if (!is_array($signer) || empty($signer)) {
21✔
600
                        throw new LibresignException($this->l10n->t('No signers'));
2✔
601
                }
602

603
                $this->validateSignerDisplayName($signer);
19✔
604
                $this->validateSignerIdentifyMethods($signer);
17✔
605
        }
606

607
        private function validateSignerDisplayName(array $signer): void {
608
                if (isset($signer['displayName']) && strlen($signer['displayName']) > 64) {
19✔
609
                        // It's an api error, don't translate
610
                        throw new LibresignException('Display name must not be longer than 64 characters');
2✔
611
                }
612
        }
613

614
        private function validateSignerIdentifyMethods(array $signer): void {
615
                $normalizedMethods = $this->normalizeIdentifyMethods($signer);
17✔
616

617
                foreach ($normalizedMethods as $method) {
12✔
618
                        $this->validateIdentifyMethodForRequest($method['name'], $method['value']);
12✔
619
                }
620
        }
621

622
        /**
623
         * @todo unify the key to be only 'identify' or only 'identifyMethods'
624
         */
625
        private function normalizeIdentifyMethods(array $signer): array {
626
                $key = array_key_exists('identifyMethods', $signer) ? 'identifyMethods' : 'identify';
17✔
627

628
                if (empty($signer[$key]) || !is_array($signer[$key])) {
17✔
629
                        throw new LibresignException('No identify methods for signer');
3✔
630
                }
631

632
                $normalizedMethods = [];
14✔
633

634
                foreach ($signer[$key] as $name => $data) {
14✔
635
                        $normalizedMethods[] = $this->normalizeIdentifyMethodEntry($key, $name, $data);
14✔
636
                }
637
                return $normalizedMethods;
12✔
638
        }
639

640
        /**
641
         * Extracted from normalizeIdentifyMethods to reduce cyclomatic complexity.
642
         */
643
        private function normalizeIdentifyMethodEntry(string $key, $name, $data): array {
644
                if ($key === 'identifyMethods') {
14✔
645
                        return $this->normalizeIdentifyMethodsStructure($data);
5✔
646
                } else {
647
                        return $this->normalizeIdentifyStructure($name, $data);
10✔
648
                }
649
        }
650

651
        private function normalizeIdentifyMethodsStructure(mixed $data): array {
652
                if (!is_array($data) || !array_key_exists('method', $data) || !array_key_exists('value', $data)) {
5✔
653
                        throw new LibresignException('Invalid identify method structure');
2✔
654
                }
655

656
                return [
3✔
657
                        'name' => $data['method'],
3✔
658
                        'value' => $data['value'],
3✔
659
                ];
3✔
660
        }
661

662
        private function normalizeIdentifyStructure(string $name, mixed $value): array {
663
                return [
10✔
664
                        'name' => $name,
10✔
665
                        'value' => $value,
10✔
666
                ];
10✔
667
        }
668

669
        private function validateIdentifyMethodForRequest(string $name, string $identifyValue): void {
670
                $identifyMethod = $this->identifyMethodService->getInstanceOfIdentifyMethod($name, $identifyValue);
14✔
671
                $identifyMethod->validateToRequest();
14✔
672

673
                $signatureMethods = $identifyMethod->getSignatureMethods();
14✔
674
                if (empty($signatureMethods)) {
14✔
675
                        // It's an api error, don't translate
676
                        throw new LibresignException('No signature methods for identify method ' . $name);
1✔
677
                }
678
        }
679

680
        public function validateExistingFile(array $data): void {
681
                if (isset($data['uuid'])) {
11✔
682
                        $this->validateFileUuid($data);
5✔
683
                        $file = $this->fileMapper->getByUuid($data['uuid']);
5✔
684
                        $this->iRequestedSignThisFile($data['userManager'], $file->getId());
5✔
685
                } elseif (isset($data['file'])) {
6✔
686
                        if (!isset($data['file']['fileId'])) {
5✔
687
                                throw new LibresignException($this->l10n->t('Invalid fileID'));
1✔
688
                        }
689
                        $this->validateLibreSignFileId($data['file']['fileId']);
4✔
690
                        $this->iRequestedSignThisFile($data['userManager'], $data['file']['fileId']);
3✔
691
                } else {
692
                        // TRANSLATORS This message is at API side. When an application or a
693
                        // developer send a structure to API without an UUID or without a
694
                        // File Object, throws this error. Normally LibreSign don't throws
695
                        // this error because the User Interface of LibreSign or send an
696
                        // UUID or a File object to API.
697
                        throw new LibresignException($this->l10n->t('Please provide either UUID or File object'));
1✔
698
                }
699
        }
700

701
        public function haveValidMail(array $data, ?int $type = null): void {
702
                if ($type === self::TYPE_TO_SIGN) {
4✔
703
                        return;
×
704
                }
705
                if (empty($data)) {
4✔
706
                        throw new LibresignException($this->l10n->t('No user data'));
1✔
707
                }
708
                if (empty($data['email'])) {
3✔
709
                        if (!empty($data['uid'])) {
1✔
710
                                $user = $this->userManager->get($data['uid']);
×
711
                                if (!$user) {
×
712
                                        throw new LibresignException($this->l10n->t('User not found.'));
×
713
                                }
714
                                if (!$user->getEMailAddress()) {
×
715
                                        // TRANSLATORS There is no email address for given user
716
                                        throw new LibresignException($this->l10n->t('User %s has no email address.', [$data['uid']]));
×
717
                                }
718
                        } else {
719
                                throw new LibresignException($this->l10n->t('Email required'));
1✔
720
                        }
721
                } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
2✔
722
                        throw new LibresignException($this->l10n->t('Invalid email'));
1✔
723
                }
724
        }
725

726
        public function validateFileUuid(array $data): void {
727
                try {
728
                        $this->fileMapper->getByUuid($data['uuid']);
7✔
729
                } catch (\Throwable) {
1✔
730
                        throw new LibresignException($this->l10n->t('Invalid UUID file'));
1✔
731
                }
732
        }
733

734
        public function validateSigner(string $uuid, ?IUser $user = null): void {
735
                $this->validateSignerUuidExists($uuid);
10✔
736
                $this->validateSignerStatus($uuid);
9✔
737
                $this->validateIdentifyMethod($uuid, $user);
7✔
738
        }
739

740
        public function validateSignerUuid(string $uuid): void {
741
                $this->validateSignerUuidExists($uuid);
×
742
        }
743

744
        /**
745
         * @throws LibresignException
746
         */
747
        private function validateSignerStatus(string $uuid): void {
748
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
9✔
749
                $status = $signRequest->getStatusEnum();
9✔
750

751
                $file = $this->fileMapper->getById($signRequest->getFileId());
9✔
752
                $this->sequentialSigningService->setFile($file);
9✔
753

754
                if ($status === \OCA\Libresign\Enum\SignRequestStatus::DRAFT) {
9✔
755
                        try {
756
                                $idDocs = $this->idDocsMapper->getByFileId($signRequest->getFileId());
2✔
757
                                if (!empty($idDocs)) {
1✔
758
                                        return;
1✔
759
                                }
760
                        } catch (\Throwable) {
1✔
761
                        }
762

763
                        throw new LibresignException(json_encode([
1✔
764
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
765
                                'errors' => [['message' => $this->l10n->t('You are not allowed to sign this document yet')]],
1✔
766
                        ]));
1✔
767
                }
768

769
                if ($status === \OCA\Libresign\Enum\SignRequestStatus::SIGNED) {
7✔
770
                        throw new LibresignException(json_encode([
×
771
                                'action' => JSActions::ACTION_DO_NOTHING,
×
772
                                'errors' => [['message' => $this->l10n->t('Document already signed')]],
×
773
                        ]));
×
774
                }
775

776
                if (
777
                        $this->sequentialSigningService->isOrderedNumericFlow()
7✔
778
                        && $this->sequentialSigningService->hasPendingLowerOrderSigners(
7✔
779
                                $signRequest->getFileId(),
7✔
780
                                $signRequest->getSigningOrder()
7✔
781
                        )
7✔
782
                ) {
783
                        throw new LibresignException(json_encode([
1✔
784
                                'action' => JSActions::ACTION_DO_NOTHING,
1✔
785
                                'errors' => [['message' => $this->l10n->t('You are not allowed to sign this document yet')]],
1✔
786
                        ]));
1✔
787
                }
788
        }
789

790
        public function validateRenewSigner(string $uuid, ?IUser $user = null): void {
791
                $this->validateSignerUuidExists($uuid);
×
792
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
×
793
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
794
                foreach ($identifyMethods as $methods) {
×
795
                        foreach ($methods as $identifyMethod) {
×
796
                                $identifyMethod->validateToRenew($user);
×
797
                        }
798
                }
799
        }
800

801
        private function validateIdentifyMethod(string $uuid, ?IUser $user = null): void {
802
                $signRequest = $this->signRequestMapper->getByUuid($uuid);
7✔
803
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
7✔
804
                foreach ($identifyMethods as $methods) {
7✔
805
                        foreach ($methods as $identifyMethod) {
4✔
806
                                $identifyMethod->validateToIdentify();
4✔
807
                        }
808
                }
809
        }
810

811
        private function validateSignerUuidExists(string $uuid): void {
812
                $this->validateUuidFormat($uuid);
10✔
813
                try {
814
                        $signRequest = $this->signRequestMapper->getByUuid($uuid);
9✔
815
                        $this->fileMapper->getById($signRequest->getFileId());
9✔
816
                } catch (DoesNotExistException) {
×
817
                        throw new LibresignException(json_encode([
×
818
                                'action' => JSActions::ACTION_DO_NOTHING,
×
819
                                'errors' => [['message' => $this->l10n->t('Invalid UUID')]],
×
820
                        ]));
×
821
                }
822
        }
823

824
        /**
825
         * @throws LibresignException
826
         */
827
        public function validateUuidFormat(string $uuid): void {
828
                if (!$uuid || !preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', $uuid)) {
12✔
829
                        throw new LibresignException(json_encode([
2✔
830
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
831
                                'errors' => [['message' => $this->l10n->t('Invalid UUID')]],
2✔
832
                        ]), Http::STATUS_NOT_FOUND);
2✔
833
                }
834
        }
835

836
        public function validateIsSignerOfFile(int $signRequestId, int $fileId): void {
837
                try {
838
                        $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
3✔
839
                } catch (\Throwable) {
1✔
840
                        throw new LibresignException($this->l10n->t('Signer not associated to this file'));
1✔
841
                }
842
        }
843

844
        public function validateUserHasNoFileWithThisType(string $uid, string $type): void {
845
                $exists = $this->idDocsMapper->getByUserAndType($uid, $type);
3✔
846
                if ($exists !== null) {
3✔
847
                        throw new LibresignException($this->l10n->t('A file of this type has been associated.'));
1✔
848
                }
849
        }
850

851
        public function canSignWithIdentificationDocumentStatus(?IUser $user, int $status): void {
852
                if ($user && $this->userCanApproveValidationDocuments($user, false)) {
7✔
853
                        return;
×
854
                }
855

856
                $allowedStatus = [
7✔
857
                        FileService::IDENTIFICATION_DOCUMENTS_DISABLED,
7✔
858
                        FileService::IDENTIFICATION_DOCUMENTS_APPROVED,
7✔
859
                ];
7✔
860
                if (!in_array($status, $allowedStatus)) {
7✔
861
                        throw new LibresignException(
3✔
862
                                $this->l10n->t('You need to have an approved identification document to sign.'),
3✔
863
                                JSActions::ACTION_SIGN_ID_DOC,
3✔
864
                        );
3✔
865
                }
866
        }
867

868
        public function validateCredentials(SignRequest $signRequest, string $identifyMethodName, string $identifyValue, string $token): void {
869
                if ($identifyMethodName === IdentifyMethodService::IDENTIFY_PASSWORD && $token === '') {
2✔
870
                        throw new LibresignException($this->l10n->t('libresign', 'Invalid password'));
2✔
871
                }
872
                $this->validateIfIdentifyMethodExists($identifyMethodName);
×
873
                if ($signRequest->getSigned()) {
×
874
                        throw new LibresignException($this->l10n->t('File already signed.'));
×
875
                }
876
                $identifyMethod = $this->resolveIdentifyMethod($signRequest, $identifyMethodName, $identifyValue);
×
877
                $identifyMethod->setCodeSentByUser($token);
×
878
                $identifyMethod->validateToSign();
×
879
        }
880

881
        private function resolveIdentifyMethod(SignRequest $signRequest, string $methodName, ?string $identifyValue): IIdentifyMethod {
882
                if (!$signRequest->getId()) {
×
883
                        return $this->identifyMethodService
×
884
                                ->setCurrentIdentifyMethod()
×
885
                                ->getInstanceOfIdentifyMethod($methodName, $identifyValue);
×
886
                }
887

888
                $methodsList = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
×
889
                $identifyMethod = $this->searchMethodByNameAndValue($methodsList, $methodName, $identifyValue);
×
890
                if ($identifyMethod) {
×
891
                        return $identifyMethod;
×
892
                }
893

894
                $signMethods = $this->identifyMethodService->getSignMethodsOfIdentifiedFactors($signRequest->getId());
×
895
                $identifyMethod = $this->searchMethodByNameAndValue($signMethods, $methodName, $identifyValue);
×
896
                if ($identifyMethod) {
×
897
                        return $identifyMethod;
×
898
                }
899

900
                if (!empty($methodsList)) {
×
901
                        return $this->getFirstAvailableMethod($methodsList);
×
902
                }
903

904
                if (!empty($signMethods)) {
×
905
                        return $this->getFirstAvailableMethod($signMethods);
×
906
                }
907

908
                throw new LibresignException($this->l10n->t('Invalid identification method'));
×
909
        }
910

911
        private function searchMethodByNameAndValue(array $methods, string $methodName, ?string $identifyValue): ?IIdentifyMethod {
912
                if (isset($methods[$methodName])) {
×
913
                        if ($identifyValue) {
×
914
                                foreach ($methods[$methodName] as $identifyMethod) {
×
915
                                        if (!$identifyMethod instanceof IIdentifyMethod) {
×
916
                                                $identifyMethod = $this->getIdentifyMethodByNameAndValue($methodName, $identifyValue);
×
917
                                        }
918
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $identifyValue) {
×
919
                                                return $identifyMethod;
×
920
                                        }
921
                                }
922
                        } else {
923
                                $identifyMethod = current($methods[$methodName]);
×
924
                                if (!$identifyMethod instanceof IIdentifyMethod) {
×
925
                                        $identifyMethod = $this->getIdentifyMethodByNameAndValue($methodName, $identifyValue);
×
926
                                }
927
                                return $identifyMethod;
×
928
                        }
929
                }
930

931
                return null;
×
932
        }
933

934
        private function getIdentifyMethodByNameAndValue(string $identifyMethodName, ?string $identifyValue): IIdentifyMethod {
935
                return $this->identifyMethodService
×
936
                        ->setCurrentIdentifyMethod()
×
937
                        ->getInstanceOfIdentifyMethod($identifyMethodName, $identifyValue);
×
938
        }
939

940
        private function getFirstAvailableMethod(array $methods): IIdentifyMethod {
941
                foreach ($methods as $methodGroup) {
×
942
                        if (!empty($methodGroup)) {
×
943
                                return current($methodGroup);
×
944
                        }
945
                }
946
                throw new LibresignException($this->l10n->t('Invalid identification method'));
×
947
        }
948

949
        public function validateIfIdentifyMethodExists(string $identifyMethod): void {
950
                if (!in_array($identifyMethod, IdentifyMethodService::IDENTIFY_METHODS)) {
8✔
951
                        // TRANSLATORS When is requested to a person to sign a file, is
952
                        // necessary identify what is the identification method. The
953
                        // identification method is used to define how will be the sign
954
                        // flow.
955
                        throw new LibresignException($this->l10n->t('Invalid identification method'));
2✔
956
                }
957
        }
958

959
        public function validateFileTypeExists(string $type): void {
960
                $profileFileTypes = $this->fileTypeMapper->getTypes();
3✔
961
                if (!array_key_exists($type, $profileFileTypes)) {
3✔
962
                        throw new LibresignException($this->l10n->t('Invalid file type.'));
1✔
963
                }
964
        }
965

966
        public function userCanApproveValidationDocuments(?IUser $user, bool $throw = true): bool {
967
                if ($user == null) {
8✔
968
                        return false;
×
969
                }
970

971
                $authorized = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
8✔
972
                if (!$authorized || !is_array($authorized) || empty($authorized)) {
8✔
973
                        $authorized = ['admin'];
5✔
974
                }
975
                $userGroups = $this->groupManager->getUserGroupIds($user);
8✔
976
                if (!array_intersect($userGroups, $authorized)) {
8✔
977
                        if ($throw) {
7✔
978
                                throw new LibresignException($this->l10n->t('You are not allowed to approve user profile documents.'));
×
979
                        }
980
                        return false;
7✔
981
                }
982
                return true;
1✔
983
        }
984

985
        private function validateDocMdpPdfRestrictions(array $data): void {
986
                if (empty($data['uuid']) || empty($data['signers'])) {
21✔
987
                        return;
19✔
988
                }
989

990
                try {
991
                        $file = $this->fileMapper->getByUuid($data['uuid']);
2✔
992
                        $this->docMdpValidator->validatePdfRestrictions($file);
2✔
NEW
993
                } catch (DoesNotExistException) {
×
994
                }
995
        }
996
}
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