Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.14% covered (warning)
82.14%
92 / 112
50.00% covered (danger)
50.00%
7 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
VisibleElementValidator
82.14% covered (warning)
82.14%
92 / 112
50.00% covered (danger)
50.00%
7 / 14
90.80
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateVisibleElements
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
8.12
 validateVisibleElement
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 validateElementSignRequestId
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
7.23
 validateElementCoordinates
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 validateElementPage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 validateElementType
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 validateVisibleElementsRelation
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
9.11
 validateSignRequestBelongsToFile
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 validateVisibleElementBelongsToFile
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 validateAuthenticatedUserIsOwnerOfPdfVisibleElement
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getEnvelopeChildSignRequests
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 validateUserHasNecessaryElements
50.00% covered (danger)
50.00%
8 / 16
0.00% covered (danger)
0.00%
0 / 1
19.12
 validateSignerIsOwnerOfPdfVisibleElement
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Validation;
10
11use OCA\Libresign\Db\FileElement;
12use OCA\Libresign\Db\FileElementMapper;
13use OCA\Libresign\Db\FileMapper;
14use OCA\Libresign\Db\SignRequest;
15use OCA\Libresign\Db\SignRequestMapper;
16use OCA\Libresign\Db\UserElementMapper;
17use OCA\Libresign\Exception\LibresignException;
18use OCA\Libresign\Service\SignerElementsService;
19use OCP\AppFramework\Db\DoesNotExistException;
20use OCP\IL10N;
21use OCP\IUser;
22
23class VisibleElementValidator {
24    public function __construct(
25        private IL10N $l10n,
26        private SignRequestMapper $signRequestMapper,
27        private FileMapper $fileMapper,
28        private FileElementMapper $fileElementMapper,
29        private UserElementMapper $userElementMapper,
30        private SignerElementsService $signerElementsService,
31        private FileInputValidator $fileInputValidator,
32    ) {
33    }
34
35    public function validateVisibleElements(?array $visibleElements, int $type): void {
36        if (!is_array($visibleElements)) {
37            throw new LibresignException($this->l10n->t('Visible elements need to be an array'));
38        }
39        if ($visibleElements && !$this->signerElementsService->isSignElementsAvailable()) {
40            throw new LibresignException($this->l10n->t('Visible elements are disabled.'));
41        }
42        foreach ($visibleElements as $element) {
43            $this->validateVisibleElement($element, $type);
44        }
45    }
46
47    public function validateVisibleElement(array $element, int $type): void {
48        $this->validateElementType($element);
49        $this->validateElementSignRequestId($element, $type);
50        $this->fileInputValidator->validateFile($element, $type);
51        $this->validateElementCoordinates($element);
52    }
53
54    public function validateElementSignRequestId(array $element, int $type): void {
55        if ($type !== FileInputValidator::TYPE_VISIBLE_ELEMENT_PDF) {
56            return;
57        }
58        if (!array_key_exists('signRequestId', $element) && !array_key_exists('uuid', $element)) {
59            throw new LibresignException($this->l10n->t('Element must be associated with a user'));
60        }
61        $getter = array_key_exists('signRequestId', $element)
62            ? fn () => $this->signRequestMapper->getById($element['signRequestId'])
63            : fn () => $this->signRequestMapper->getByUuid($element['uuid']);
64        try {
65            $signRequest = $getter();
66        } catch (\Throwable) {
67            throw new LibresignException($this->l10n->t('User not found for element.'));
68        }
69
70        if (!$signRequest->getParticipantRoleEnum()->canSign()) {
71            // TRANSLATORS Validation error when a visible signature element is assigned to an observer, who cannot sign.
72            throw new LibresignException($this->l10n->t('Observers cannot have visible signature elements'));
73        }
74    }
75
76    public function validateElementCoordinates(array $element): void {
77        if (!array_key_exists('coordinates', $element)) {
78            return;
79        }
80        $this->validateElementPage($element);
81        foreach ($element['coordinates'] as $type => $value) {
82            if (!in_array($type, ['llx', 'lly', 'urx', 'ury', 'width', 'height', 'left', 'top'], true)) {
83                continue;
84            }
85            if (!is_int($value)) {
86                throw new LibresignException($this->l10n->t('Coordinate %s must be an integer', [$type]));
87            }
88            if ($value < 0) {
89                throw new LibresignException($this->l10n->t('Object outside the page margin'));
90            }
91        }
92    }
93
94    public function validateElementPage(array $element): void {
95        if (!array_key_exists('page', $element['coordinates'])) {
96            return;
97        }
98        if (!is_int($element['coordinates']['page'])) {
99            throw new LibresignException($this->l10n->t('Page number must be an integer'));
100        }
101        if ($element['coordinates']['page'] < 1) {
102            throw new LibresignException($this->l10n->t('Page must be equal to or greater than 1'));
103        }
104    }
105
106    public function validateElementType(array $element): void {
107        if (!array_key_exists('type', $element)) {
108            if (!array_key_exists('elementId', $element)) {
109                throw new LibresignException($this->l10n->t('Element needs a type'));
110            }
111            return;
112        }
113        if (!in_array($element['type'], ['signature', 'initial', 'date', 'datetime', 'text'], true)) {
114            throw new LibresignException($this->l10n->t('Invalid element type'));
115        }
116    }
117
118    public function validateVisibleElementsRelation(array $list, SignRequest $signRequest, ?IUser $user): void {
119        $canCreateSignature = $this->signerElementsService->canCreateSignature();
120        $childSignRequests = $this->getEnvelopeChildSignRequests($signRequest);
121        $childSignRequestIds = array_map(fn (SignRequest $sr) => $sr->getId(), $childSignRequests);
122        foreach ($list as $elements) {
123            if (!array_key_exists('documentElementId', $elements)) {
124                throw new LibresignException($this->l10n->t('Field %s not found', ['documentElementId']));
125            }
126            if ($canCreateSignature && !array_key_exists('profileNodeId', $elements)) {
127                throw new LibresignException($this->l10n->t('Field %s not found', ['profileNodeId']));
128            }
129            $this->validateSignerIsOwnerOfPdfVisibleElement($elements['documentElementId'], $signRequest, $childSignRequestIds);
130            if ($canCreateSignature) {
131                if (!is_int($elements['profileNodeId'])) {
132                    // TRANSLATORS Validation error when the visible signature profile node ID is not an integer.
133                    throw new LibresignException($this->l10n->t('Field %s must be an integer', ['profileNodeId']));
134                }
135                try {
136                    if ($user instanceof IUser) {
137                        $this->userElementMapper->findOne(['node_id' => $elements['profileNodeId'], 'user_id' => $user->getUID()]);
138                    } else {
139                        $this->signerElementsService->getElementFromSessionByNodeId($elements['profileNodeId']);
140                    }
141                } catch (DoesNotExistException) {
142                    throw new LibresignException($this->l10n->t('Field %s does not belong to user', [$elements['profileNodeId']]));
143                }
144            }
145        }
146        $this->validateUserHasNecessaryElements($signRequest, $user, $list, $childSignRequests);
147    }
148
149    public function validateSignRequestBelongsToFile(int $signRequestId, string $uuid): void {
150        try {
151            $file = $this->fileMapper->getByUuid($uuid);
152            $signRequest = $this->signRequestMapper->getById($signRequestId);
153        } catch (DoesNotExistException) {
154            // TRANSLATORS Validation error when a signer is not associated with the requested document.
155            throw new LibresignException($this->l10n->t('Signer not associated to this file'));
156        }
157        if ($signRequest->getFileId() !== $file->getId()) {
158            // TRANSLATORS Validation error when a signer is not associated with the requested document.
159            throw new LibresignException($this->l10n->t('Signer not associated to this file'));
160        }
161    }
162
163    public function validateVisibleElementBelongsToFile(int $documentElementId, string $uuid, bool $allowEnvelopeChild = false): void {
164        try {
165            $file = $this->fileMapper->getByUuid($uuid);
166            $element = $this->fileElementMapper->getById($documentElementId);
167            if ($allowEnvelopeChild && $file->isEnvelope() && $element->getFileId() !== $file->getId()) {
168                $childFile = $this->fileMapper->getById($element->getFileId());
169                if (!$childFile->isEnvelope() && $childFile->getParentFileId() === $file->getId()) {
170                    return;
171                }
172            }
173        } catch (DoesNotExistException) {
174            // TRANSLATORS Authorization error when a visible signing field is missing or belongs to a different document. %s is the numeric field ID.
175            throw new LibresignException($this->l10n->t('Field %s does not belong to user', [(string)$documentElementId]));
176        }
177        if ($element->getFileId() !== $file->getId()) {
178            // TRANSLATORS Authorization error when a visible signing field is missing or belongs to a different document. %s is the numeric field ID.
179            throw new LibresignException($this->l10n->t('Field %s does not belong to user', [(string)$documentElementId]));
180        }
181    }
182
183    public function validateAuthenticatedUserIsOwnerOfPdfVisibleElement(int $documentElementId, string $uid): void {
184        try {
185            $documentElement = $this->fileElementMapper->getById($documentElementId);
186            $signRequest = $this->signRequestMapper->getById($documentElement->getSignRequestId());
187            $file = $this->fileMapper->getById($signRequest->getFileId());
188        } catch (DoesNotExistException) {
189            // TRANSLATORS Authorization error when a visible signing field is missing or cannot be changed by this user. %s is the numeric field ID.
190            throw new LibresignException($this->l10n->t('Field %s does not belong to user', [(string)$documentElementId]));
191        }
192        if ($file->getUserId() !== $uid) {
193            // TRANSLATORS Authorization error when a visible signing field is missing or cannot be changed by this user. %s is the numeric field ID.
194            throw new LibresignException($this->l10n->t('Field %s does not belong to user', [(string)$documentElementId]));
195        }
196    }
197
198    /** @return SignRequest[] */
199    private function getEnvelopeChildSignRequests(SignRequest $signRequest): array {
200        $file = $this->fileMapper->getById($signRequest->getFileId());
201        if (!$file->isEnvelope()) {
202            return [];
203        }
204        return $this->signRequestMapper->getByEnvelopeChildrenAndIdentifyMethod($file->getId(), $signRequest->getId());
205    }
206
207    /** @param SignRequest[] $childSignRequests */
208    private function validateUserHasNecessaryElements(SignRequest $signRequest, ?IUser $user, array $list = [], array $childSignRequests = []): void {
209        $fileElements = $this->fileElementMapper->getByFileIdAndSignRequestId($signRequest->getFileId(), $signRequest->getId());
210        if (empty($fileElements) && !empty($childSignRequests)) {
211            foreach ($childSignRequests as $childSr) {
212                $fileElements = array_merge($fileElements, $this->fileElementMapper->getByFileIdAndSignRequestId($childSr->getFileId(), $childSr->getId()));
213            }
214        }
215        $total = array_filter($fileElements, function (FileElement $fileElement) use ($list, $user): bool {
216            $found = array_filter($list, fn ($item): bool => $item['documentElementId'] === $fileElement->getId());
217            if (!$found && $this->signerElementsService->canCreateSignature()) {
218                try {
219                    if (!$user instanceof IUser) {
220                        throw new \RuntimeException();
221                    }
222                    $this->userElementMapper->findMany(['user_id' => $user->getUID(), 'type' => $fileElement->getType()]);
223                } catch (\Throwable) {
224                    throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
225                }
226            }
227            return true;
228        });
229        if (count($total) !== count($fileElements)) {
230            throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.'));
231        }
232    }
233
234    /** @param int[] $childSignRequestIds */
235    private function validateSignerIsOwnerOfPdfVisibleElement(int $documentElementId, SignRequest $signRequest, array $childSignRequestIds = []): void {
236        $documentElement = $this->fileElementMapper->getById($documentElementId);
237        if ($documentElement->getSignRequestId() === $signRequest->getId()) {
238            return;
239        }
240        if (!empty($childSignRequestIds) && in_array($documentElement->getSignRequestId(), $childSignRequestIds, true)) {
241            return;
242        }
243        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
244    }
245}