Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.86% covered (warning)
76.86%
93 / 121
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
EnvelopeAssembler
76.86% covered (warning)
76.86%
93 / 121
33.33% covered (danger)
33.33%
1 / 3
42.91
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
 buildEnvelopeChildData
82.88% covered (warning)
82.88%
92 / 111
0.00% covered (danger)
0.00%
0 / 1
29.39
 getSha256FromResource
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3declare(strict_types=1);
4
5/**
6 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7 * SPDX-License-Identifier: AGPL-3.0-or-later
8 */
9
10namespace OCA\Libresign\Service\File;
11
12use DateTimeInterface;
13use OCA\Libresign\Db\File;
14use OCA\Libresign\Db\FileMapper;
15use OCA\Libresign\Db\SignRequest;
16use OCA\Libresign\Db\SignRequestMapper;
17use OCA\Libresign\Service\FileElementService;
18use OCA\Libresign\Service\FolderService;
19use OCA\Libresign\Service\IdentifyMethodService;
20use OCA\Libresign\Service\SignatureRejection\RejectionViewer;
21use OCA\Libresign\Service\SignatureRejection\SignatureRejectionVisibilityService;
22use OCP\IURLGenerator;
23use Psr\Log\LoggerInterface;
24
25class EnvelopeAssembler {
26    public function __construct(
27        private SignRequestMapper $signRequestMapper,
28        private IdentifyMethodService $identifyMethodService,
29        private FileMapper $fileMapper,
30        private FolderService $folderService,
31        private IURLGenerator $urlGenerator,
32        private SignersLoader $signersLoader,
33        private ?CertificateChainService $certificateChainService,
34        private \OCA\Libresign\Handler\SignEngine\Pkcs12Handler $pkcs12Handler,
35        private LoggerInterface $logger,
36        private FileElementService $fileElementService,
37        private SignatureRejectionVisibilityService $signatureRejectionVisibilityService,
38    ) {
39    }
40
41    public function buildEnvelopeChildData(File $childFile, \OCA\Libresign\Service\File\FileResponseOptions $options): \stdClass {
42        $fileData = new \stdClass();
43        $fileData->id = $childFile->getId();
44        $fileData->uuid = $childFile->getUuid();
45        $fileData->name = $childFile->getName();
46        $fileData->status = $childFile->getStatus();
47        $fileData->statusText = $this->fileMapper->getTextOfStatus($childFile->getStatus());
48        $fileData->nodeId = $childFile->getNodeId();
49        $fileData->file = $this->urlGenerator->linkToRoute('libresign.page.getPdf', ['uuid' => $childFile->getUuid()]);
50        $childMetadata = $childFile->getMetadata() ?? [];
51        $fileData->totalPages = (int)($childMetadata['p'] ?? 0);
52        $fileData->pdfVersion = (string)($childMetadata['pdfVersion'] ?? '');
53
54        $childMetadata = ValidationMetadataNormalizer::normalize($childMetadata, $childFile->getName(), $fileData->totalPages);
55        $fileData->metadata = $childMetadata;
56
57        $nodeId = $childFile->getSignedNodeId() ?: $childFile->getNodeId();
58        $fileNode = $this->folderService->getReadableNodeById($childFile->getUserId(), $nodeId);
59        if ($fileNode instanceof \OCP\Files\File) {
60            if (method_exists($fileNode, 'getSize')) {
61                $fileData->size = $fileNode->getSize();
62            }
63            if (method_exists($fileNode, 'getMimeType')) {
64                $fileData->mime = $fileNode->getMimeType();
65            }
66        }
67        $fileData->size ??= 0;
68
69        $fileData->signers = [];
70        $fileData->visibleElements = [];
71
72        $signRequests = $this->signRequestMapper->getByFileId($childFile->getId());
73        if (empty($signRequests)) {
74            return $fileData;
75        }
76        $signRequestIds = array_column(array_map(fn ($sr) => ['id' => $sr->getId()], $signRequests), 'id');
77        $identifyMethodsBatch = $this->identifyMethodService
78            ->setIsRequest(false)
79            ->getIdentifyMethodsFromSignRequestIds($signRequestIds);
80
81        // A child document follows the same rejection visibility rules as a
82        // single file: the viewer must be known for every signer first.
83        $viewer = new RejectionViewer(
84            $options->getMe() !== null && $options->getMe()->getUID() === $childFile->getUserId(),
85            array_filter(
86                $signRequests,
87                fn (SignRequest $candidate): bool => $options->isViewerOfSigner($identifyMethodsBatch[$candidate->getId()] ?? []),
88            ),
89        );
90        $hiddenRejection = $this->signatureRejectionVisibilityService->hasHiddenRejection($childFile, $signRequests, $viewer);
91
92        foreach ($signRequests as $signRequest) {
93            $identifyMethods = $identifyMethodsBatch[$signRequest->getId()] ?? [];
94            $identifyMethodsArray = [];
95            $signerUid = null;
96            foreach ($identifyMethods as $methods) {
97                foreach ($methods as $identifyMethod) {
98                    $entity = $identifyMethod->getEntity();
99                    $identifyMethodsArray[] = [
100                        'method' => $entity->getIdentifierKey(),
101                        'value' => $entity->getIdentifierValue(),
102                        'requirement' => $entity->getRequirement(),
103                    ];
104                    $signerUid ??= $entity->getUniqueIdentifier();
105                }
106            }
107
108            $email = '';
109            foreach ($identifyMethods[IdentifyMethodService::IDENTIFY_EMAIL] ?? [] as $identifyMethod) {
110                $entity = $identifyMethod->getEntity();
111                if ($entity->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL) {
112                    $email = $entity->getIdentifierValue();
113                    break;
114                }
115            }
116
117            $signed = null;
118            if ($signRequest->getSigned()) {
119                $signed = $signRequest->getSigned()->format(DateTimeInterface::ATOM);
120            }
121
122            $displayName = $signRequest->getDisplayName();
123            if ($displayName === '' && $email !== '') {
124                $displayName = $email;
125            }
126
127            $signer = new \stdClass();
128            $signer->signRequestId = $signRequest->getId();
129            $signer->displayName = $displayName;
130            $signer->email = $email;
131            $signer->uid = $signerUid;
132            $signer->signed = $signed;
133            $this->signatureRejectionVisibilityService->presentSigner(
134                $signRequest,
135                $childFile,
136                $viewer,
137                $hiddenRejection,
138            )->applyToObject($signer);
139            $signer->identifyMethods = $identifyMethodsArray;
140            $signer->metadata = $signRequest->getMetadata();
141            $signer->visibleElements = [];
142            $fileData->signers[] = $signer;
143        }
144
145        if ($options->isShowVisibleElements()) {
146            $childMetadata = $childFile->getMetadata();
147            $formattedElementsBySigner = [];
148            foreach ($this->signRequestMapper->getVisibleElementsFromSigners($signRequests) as $signRequestId => $row) {
149                if (empty($row)) {
150                    continue;
151                }
152                $formattedElementsBySigner[$signRequestId] = $this->fileElementService->formatVisibleElements($row, $childMetadata);
153                $fileData->visibleElements = array_merge(
154                    $formattedElementsBySigner[$signRequestId],
155                    $fileData->visibleElements
156                );
157            }
158            foreach ($fileData->signers as $signer) {
159                $signer->visibleElements = $formattedElementsBySigner[$signer->signRequestId] ?? [];
160            }
161        }
162
163        if ($options->isValidateFile() && $childFile->getSignedNodeId()) {
164            try {
165                $fileNode = $this->folderService->getReadableNodeById($childFile->getUserId(), $childFile->getSignedNodeId());
166                if ($fileNode instanceof \OCP\Files\File) {
167                    if ($this->certificateChainService !== null) {
168                        $certData = $this->certificateChainService->getCertificateChain($fileNode, $childFile, $options);
169                    } else {
170                        $resource = $fileNode->fopen('rb');
171                        if (!is_resource($resource)) {
172                            throw new \RuntimeException('unable to open signed file stream');
173                        }
174                        $sha256 = $this->getSha256FromResource($resource);
175                        rewind($resource);
176                        $this->pkcs12Handler->setIsLibreSignFile($sha256 === $childFile->getSignedHash());
177                        $this->pkcs12Handler->setPolicyUserIdForValidation($childFile->getUserId());
178                        $certData = $this->pkcs12Handler->getCertificateChain($resource);
179                        fclose($resource);
180                    }
181                    if (!empty($certData)) {
182                        $this->signersLoader->loadSignersFromCertData($fileData, $certData, $options->getHost());
183                    }
184                }
185            } catch (\Exception $e) {
186                $this->logger->warning('Failed to load envelope child certificate chain: ' . $e->getMessage());
187            }
188        }
189
190        return $fileData;
191    }
192
193    private function getSha256FromResource($resource): string {
194        if (!is_resource($resource)) {
195            return '';
196        }
197
198        $hashContext = hash_init('sha256');
199        while (!feof($resource)) {
200            $buffer = fread($resource, 8192);
201            if ($buffer === false) {
202                break;
203            }
204            hash_update($hashContext, $buffer);
205        }
206        return hash_final($hashContext);
207    }
208
209}