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

LibreSign / libresign / 20882165631

10 Jan 2026 05:56PM UTC coverage: 44.646%. First build
20882165631

Pull #6433

github

web-flow
Merge eead2e4b3 into ecd36974e
Pull Request #6433: refactor: move all constants to FileStatus enum

31 of 56 new or added lines in 16 files covered. (55.36%)

6742 of 15101 relevant lines covered (44.65%)

5.01 hits per line

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

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

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

9
namespace OCA\Libresign\Helper;
10

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

580
                $normalizedMethods = [];
11✔
581

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

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

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

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

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

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

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

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

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

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

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

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

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

736
        public function validateSigner(string $uuid, ?IUser $user = null): void {
737
                $this->validateSignerUuidExists($uuid);
5✔
738
                $this->validateSignerStatus($uuid);
4✔
739
                $this->validateIdentifyMethod($uuid, $user);
4✔
740
        }
741

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

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

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

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

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

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

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

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

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

825
        public function canSignWithIdentificationDocumentStatus(?IUser $user, int $status): void {
826
                if ($user && $this->userCanApproveValidationDocuments($user, false)) {
3✔
827
                        return;
×
828
                }
829

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

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

849
        private function resolveIdentifyMethod(SignRequest $signRequest, string $methodName, ?string $identifyValue): IIdentifyMethod {
850
                if (!$signRequest->getId()) {
2✔
851
                        return $this->identifyMethodService
×
852
                                ->setCurrentIdentifyMethod()
×
853
                                ->getInstanceOfIdentifyMethod($methodName, $identifyValue);
×
854
                }
855

856
                $methodsList = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
2✔
857
                $identifyMethod = $this->searchMethodByNameAndValue($methodsList, $methodName, $identifyValue);
2✔
858
                if ($identifyMethod) {
2✔
859
                        return $identifyMethod;
×
860
                }
861

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

868
                if (!empty($methodsList)) {
2✔
869
                        return $this->getFirstAvailableMethod($methodsList);
2✔
870
                }
871

872
                if (!empty($signMethods)) {
×
873
                        return $this->getFirstAvailableMethod($signMethods);
×
874
                }
875

876
                throw new LibresignException($this->l10n->t('Invalid identification method'));
×
877
        }
878

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

899
                return null;
2✔
900
        }
901

902
        private function getIdentifyMethodByNameAndValue(string $identifyMethodName, ?string $identifyValue): IIdentifyMethod {
903
                return $this->identifyMethodService
×
904
                        ->setCurrentIdentifyMethod()
×
905
                        ->getInstanceOfIdentifyMethod($identifyMethodName, $identifyValue);
×
906
        }
907

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

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

927
        public function validateFileTypeExists(string $type): void {
928
                $profileFileTypes = $this->fileTypeMapper->getTypes();
3✔
929
                if (!array_key_exists($type, $profileFileTypes)) {
3✔
930
                        throw new LibresignException($this->l10n->t('Invalid file type.'));
1✔
931
                }
932
        }
933

934
        public function userCanApproveValidationDocuments(?IUser $user, bool $throw = true): bool {
935
                if ($user == null) {
4✔
936
                        return false;
×
937
                }
938

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