Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.90% covered (warning)
78.90%
86 / 109
75.00% covered (warning)
75.00%
9 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
IdDocsService
78.90% covered (warning)
78.90%
86 / 109
75.00% covered (warning)
75.00%
9 / 12
35.37
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
 validateTypeOfFile
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 validateIdDoc
31.25% covered (danger)
31.25%
5 / 16
0.00% covered (danger)
0.00%
0 / 1
5.92
 normalizeNodeIds
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 validateIdDocs
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addIdDocs
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 addFilesToDocumentFolder
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 getOwnerOfSignedFile
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 list
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 deleteIdDoc
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 deleteIdDocBySignRequest
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getIdDocsByUser
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service;
10
11use OCA\Libresign\AppInfo\Application;
12use OCA\Libresign\Db\FileMapper;
13use OCA\Libresign\Db\FileTypeMapper;
14use OCA\Libresign\Db\IdDocsMapper;
15use OCA\Libresign\Db\IdentifyMethod;
16use OCA\Libresign\Db\IdentifyMethodMapper;
17use OCA\Libresign\Db\SignRequest;
18use OCA\Libresign\Db\SignRequest as SignRequestEntity;
19use OCA\Libresign\Db\SignRequestMapper;
20use OCA\Libresign\Enum\IdentifyMethodRequirement;
21use OCA\Libresign\Exception\LibresignException;
22use OCA\Libresign\Service\Validation\FileInputValidator;
23use OCA\Libresign\Service\Validation\IdentityDocumentValidator;
24use OCP\AppFramework\Utility\ITimeFactory;
25use OCP\IAppConfig;
26use OCP\IL10N;
27use OCP\IUser;
28use OCP\IUserManager;
29use Sabre\DAV\UUIDUtil;
30
31class IdDocsService {
32
33    public function __construct(
34        private IL10N $l10n,
35        private FileTypeMapper $fileTypeMapper,
36        private FileInputValidator $fileInputValidator,
37        private IdentityDocumentValidator $identityDocumentValidator,
38        private RequestSignatureService $requestSignatureService,
39        private IdDocsMapper $idDocsMapper,
40        private FileMapper $fileMapper,
41        private SignRequestMapper $signRequestMapper,
42        private IdentifyMethodMapper $identifyMethodMapper,
43        private ITimeFactory $timeFactory,
44        private IAppConfig $appConfig,
45        private IUserManager $userManager,
46    ) {
47    }
48
49    private function validateTypeOfFile(int $fileIndex, array $file): void {
50        $profileFileTypes = $this->fileTypeMapper->getTypes();
51        if (!array_key_exists($file['type'], $profileFileTypes)) {
52            throw new LibresignException(json_encode([
53                'type' => 'danger',
54                'file' => $fileIndex,
55                // TRANSLATORS Error shown when an uploaded identity document has an unsupported file type.
56                'message' => $this->l10n->t('Invalid file type.')
57            ]));
58        }
59    }
60
61    private function validateIdDoc(int $fileIndex, array $file, IUser $user): void {
62        $profileFileTypes = $this->fileTypeMapper->getTypes();
63        if (!array_key_exists($file['type'], $profileFileTypes)) {
64            throw new LibresignException(json_encode([
65                'type' => 'danger',
66                'file' => $fileIndex,
67                // TRANSLATORS Error shown when an uploaded identity document has an unsupported file type.
68                'message' => $this->l10n->t('Invalid file type.')
69            ]));
70        }
71
72        try {
73            $this->identityDocumentValidator->validateFileTypeExists($file['type']);
74            $this->fileInputValidator->validateNewFile($file, FileInputValidator::TYPE_ACCOUNT_DOCUMENT, $user);
75            $this->identityDocumentValidator->validateUserHasNoFileWithThisType($user->getUID(), $file['type']);
76        } catch (\Exception $e) {
77            throw new LibresignException(json_encode([
78                'type' => 'danger',
79                'file' => $fileIndex,
80                'message' => $e->getMessage()
81            ]));
82        }
83    }
84
85    /**
86     * `file` of each entry is the HTTP `LibresignNewFile` payload; its node
87     * id is normalized once here, the same boundary the signature request has.
88     */
89    private function normalizeNodeIds(array $files): array {
90        foreach ($files as $fileIndex => $fileData) {
91            if (!is_array($fileData) || !is_array($fileData['file'] ?? null)) {
92                continue;
93            }
94            try {
95                $files[$fileIndex]['file'] = $this->fileInputValidator->normalizeNodeId($fileData['file'], FileInputValidator::TYPE_ACCOUNT_DOCUMENT);
96            } catch (LibresignException $e) {
97                throw new LibresignException(json_encode([
98                    'type' => 'danger',
99                    'file' => $fileIndex,
100                    'message' => $e->getMessage(),
101                ]));
102            }
103        }
104        return $files;
105    }
106
107    public function validateIdDocs(array $files, IUser $user): void {
108        foreach ($files as $fileIndex => $file) {
109            $this->validateTypeOfFile($fileIndex, $file);
110            $this->validateIdDoc($fileIndex, $file, $user);
111        }
112    }
113
114    public function addIdDocs(array $files, IUser $user): void {
115        $files = $this->normalizeNodeIds($files);
116        $this->validateIdDocs($files, $user);
117        foreach ($files as $fileData) {
118            $dataToSave = $fileData;
119            $dataToSave['userManager'] = $user;
120            $dataToSave['name'] = $fileData['name'] ?? $fileData['type'];
121            $file = $this->requestSignatureService->saveFile($dataToSave);
122
123            $signRequest = new SignRequestEntity();
124            $signRequest->setFileId($file->getId());
125            $signRequest->setDisplayName($user->getDisplayName());
126            $signRequest->setUuid(UUIDUtil::getUUID());
127            $signRequest->setCreatedAt($this->timeFactory->getDateTime());
128            $this->signRequestMapper->insert($signRequest);
129
130            $identifyMethod = new IdentifyMethod();
131            $identifyMethod->setSignRequestId($signRequest->getId());
132            $identifyMethod->setIdentifierKey(IdentifyMethodService::IDENTIFY_ACCOUNT);
133            $identifyMethod->setIdentifierValue($user->getUID());
134            $identifyMethod->setRequirement(IdentifyMethodRequirement::REQUIRED->value);
135            $this->identifyMethodMapper->insert($identifyMethod);
136
137            $this->idDocsMapper->save($file->getId(), $signRequest->getId(), $user->getUID(), $fileData['type']);
138        }
139    }
140
141    public function addFilesToDocumentFolder(
142        array $files,
143        SignRequest $signRequest,
144    ): void {
145        $files = $this->normalizeNodeIds($files);
146        foreach ($files as $fileIndex => $file) {
147            $this->validateTypeOfFile($fileIndex, $file);
148        }
149        // Store the documents under the owner of the file being signed, where
150        // later lookups by the stored user_id expect them; without an IUser the
151        // node would be written to the unauthenticated appdata folder instead.
152        $owner = $this->getOwnerOfSignedFile($signRequest);
153        foreach ($files as $fileData) {
154            $dataToSave = $fileData;
155            $dataToSave['signRequest'] = $signRequest;
156            $dataToSave['name'] = $fileData['name'] ?? $fileData['type'];
157            if ($owner instanceof IUser) {
158                $dataToSave['userManager'] = $owner;
159            }
160            $file = $this->requestSignatureService->saveFile($dataToSave);
161
162            $this->idDocsMapper->save($file->getId(), $signRequest->getId(), null, $fileData['type']);
163        }
164    }
165
166    private function getOwnerOfSignedFile(SignRequest $signRequest): ?IUser {
167        $signedFileId = $signRequest->getFileId();
168        if (!$signedFileId) {
169            return null;
170        }
171        try {
172            $signedFile = $this->fileMapper->getById($signedFileId);
173        } catch (\OCP\AppFramework\Db\DoesNotExistException) {
174            return null;
175        }
176        return $this->userManager->get($signedFile->getUserId());
177    }
178
179    public function list(array $filter, ?int $page = null, ?int $length = null, array $sort = []): array {
180        $page ??= 1;
181        $length ??= (int)$this->appConfig->getValueInt(Application::APP_ID, 'length_of_page', 100);
182        $data = $this->idDocsMapper->list($filter, $page, $length, $sort);
183        $data['pagination']->setRouteName('ocs.libresign.File.list');
184        $pagination = $data['pagination']->getPagination($page, $length, $filter);
185        return [
186            'data' => $data['data'],
187            'pagination' => $pagination,
188        ];
189    }
190
191    public function deleteIdDoc(int $nodeId, IUser $user): void {
192        if ($this->identityDocumentValidator->userCanApproveValidationDocuments($user, false)) {
193            $idDocs = $this->idDocsMapper->getByNodeId($nodeId);
194        } else {
195            $this->identityDocumentValidator->validateIdDocIsOwnedByUser($nodeId, $user->getUID());
196            $idDocs = $this->idDocsMapper->getByUserIdAndNodeId($user->getUID(), $nodeId);
197        }
198        $this->idDocsMapper->delete($idDocs);
199        $file = $this->fileMapper->getById($idDocs->getFileId());
200        $this->fileMapper->delete($file);
201    }
202
203    public function deleteIdDocBySignRequest(int $nodeId, SignRequest $signRequest): void {
204        $this->identityDocumentValidator->validateIdDocBelongsToSignRequest($nodeId, $signRequest->getId());
205        $idDocs = $this->idDocsMapper->getBySignRequestIdAndNodeId($signRequest->getId(), $nodeId);
206        $this->idDocsMapper->delete($idDocs);
207        $file = $this->fileMapper->getById($idDocs->getFileId());
208        $this->fileMapper->delete($file);
209    }
210
211    public function getIdDocsByUser(IUser $user): array {
212        $idDocs = $this->idDocsMapper->getByUserId($user->getUID());
213        $files = [];
214        foreach ($idDocs as $idDoc) {
215            $file = $this->fileMapper->getById($idDoc->getFileId());
216            $files[] = [
217                'nodeId' => $file->getNodeId(),
218                'fileId' => $idDoc->getFileId(),
219                'type' => $idDoc->getFileType(),
220                'name' => $file->getName(),
221            ];
222        }
223        return $files;
224    }
225}