Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
76.86% |
93 / 121 |
|
33.33% |
1 / 3 |
CRAP | |
0.00% |
0 / 1 |
| EnvelopeAssembler | |
76.86% |
93 / 121 |
|
33.33% |
1 / 3 |
42.91 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| buildEnvelopeChildData | |
82.88% |
92 / 111 |
|
0.00% |
0 / 1 |
29.39 | |||
| getSha256FromResource | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** |
| 6 | * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors |
| 7 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 8 | */ |
| 9 | |
| 10 | namespace OCA\Libresign\Service\File; |
| 11 | |
| 12 | use DateTimeInterface; |
| 13 | use OCA\Libresign\Db\File; |
| 14 | use OCA\Libresign\Db\FileMapper; |
| 15 | use OCA\Libresign\Db\SignRequest; |
| 16 | use OCA\Libresign\Db\SignRequestMapper; |
| 17 | use OCA\Libresign\Service\FileElementService; |
| 18 | use OCA\Libresign\Service\FolderService; |
| 19 | use OCA\Libresign\Service\IdentifyMethodService; |
| 20 | use OCA\Libresign\Service\SignatureRejection\RejectionViewer; |
| 21 | use OCA\Libresign\Service\SignatureRejection\SignatureRejectionVisibilityService; |
| 22 | use OCP\IURLGenerator; |
| 23 | use Psr\Log\LoggerInterface; |
| 24 | |
| 25 | class 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 | } |