Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
50 / 50 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
| SignatureRejectionVisibilityService | |
100.00% |
50 / 50 |
|
100.00% |
4 / 4 |
22 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasHiddenRejection | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
| presentSigner | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
6 | |||
| buildSignerRejection | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
10 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | /** |
| 5 | * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors |
| 6 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 7 | */ |
| 8 | |
| 9 | namespace OCA\Libresign\Service\SignatureRejection; |
| 10 | |
| 11 | use DateTimeInterface; |
| 12 | use OCA\Libresign\Db\File as FileEntity; |
| 13 | use OCA\Libresign\Db\SignRequest as SignRequestEntity; |
| 14 | use OCA\Libresign\Enum\SignerDisplayStatus; |
| 15 | use OCA\Libresign\Enum\SignRequestStatus; |
| 16 | use OCP\IL10N; |
| 17 | |
| 18 | /** |
| 19 | * Decides how much of a rejection is disclosed to whoever is reading the API. |
| 20 | * |
| 21 | * Everything is hidden by default: only the person who requested the signature |
| 22 | * and the signer who rejected always see the whole record. Other readers see |
| 23 | * what the document policy discloses to their audience (see RejectionViewer). |
| 24 | * |
| 25 | * A comment the signer marked as private is never disclosed to them, whatever |
| 26 | * the policy says: the rejection changes the workflow so its status may need to |
| 27 | * be shared, but the words the signer wrote are their own. |
| 28 | */ |
| 29 | class SignatureRejectionVisibilityService { |
| 30 | public function __construct( |
| 31 | private SignatureRejectionPolicyService $rejectionPolicyService, |
| 32 | private IL10N $l10n, |
| 33 | ) { |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Whether the file holds a rejection the viewer may not know about. |
| 38 | * |
| 39 | * A rejection is visible to the requester of the file and to the signer |
| 40 | * who rejected; everybody else only sees it when the policy discloses it |
| 41 | * to their audience. When one is hidden, redacting that signer alone would |
| 42 | * still point at them by comparison with the others, so the callers |
| 43 | * present every unsigned signer of the file the same way. |
| 44 | * |
| 45 | * @param SignRequestEntity[] $signers every sign request of the file |
| 46 | */ |
| 47 | public function hasHiddenRejection(?FileEntity $libreSignFile, array $signers, RejectionViewer $viewer): bool { |
| 48 | $config = null; |
| 49 | foreach ($signers as $signer) { |
| 50 | if ($signer->getStatusEnum() !== SignRequestStatus::REJECTED) { |
| 51 | continue; |
| 52 | } |
| 53 | if ($viewer->isPrivilegedFor($signer)) { |
| 54 | continue; |
| 55 | } |
| 56 | $config ??= $this->rejectionPolicyService->getConfig($libreSignFile); |
| 57 | if (!$config->isRejectionVisibleTo($viewer->getAudience())) { |
| 58 | return true; |
| 59 | } |
| 60 | } |
| 61 | return false; |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * How one signer is presented to the viewer. |
| 66 | * |
| 67 | * While a rejection is hidden, every unsigned signer is redacted, the |
| 68 | * viewer's own pending entry included: if they kept their real status |
| 69 | * they could tell who rejected by comparison. The only entry the viewer |
| 70 | * keeps is their own rejection, which they already know about. |
| 71 | * |
| 72 | * @param bool $hiddenRejectionInFile the result of hasHiddenRejection() for the file |
| 73 | */ |
| 74 | public function presentSigner( |
| 75 | SignRequestEntity $signer, |
| 76 | ?FileEntity $libreSignFile, |
| 77 | RejectionViewer $viewer, |
| 78 | bool $hiddenRejectionInFile, |
| 79 | ): SignerPresentation { |
| 80 | $status = $signer->getStatusEnum(); |
| 81 | // Only a participant who could have rejected is redacted: a signed |
| 82 | // signer and an observer say nothing about who rejected, and the |
| 83 | // viewer keeps their own rejection. |
| 84 | $couldHaveRejected = $status !== SignRequestStatus::SIGNED && $status !== SignRequestStatus::OBSERVING; |
| 85 | $ownRejection = $status === SignRequestStatus::REJECTED && $viewer->isPrivilegedFor($signer); |
| 86 | if ($hiddenRejectionInFile && $couldHaveRejected && !$ownRejection) { |
| 87 | return new SignerPresentation( |
| 88 | SignerDisplayStatus::NOT_SIGNED, |
| 89 | null, |
| 90 | SignerDisplayStatus::NOT_SIGNED->getLabel($this->l10n), |
| 91 | null, |
| 92 | ); |
| 93 | } |
| 94 | |
| 95 | return new SignerPresentation( |
| 96 | SignerDisplayStatus::fromSignRequestStatus($status), |
| 97 | $status->value, |
| 98 | $status->getLabel($this->l10n), |
| 99 | $this->buildSignerRejection($signer, $libreSignFile, $viewer), |
| 100 | ); |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * @return array{rejectedAt: string, comment?: string, commentPrivate?: bool}|null |
| 105 | */ |
| 106 | public function buildSignerRejection( |
| 107 | SignRequestEntity $signRequest, |
| 108 | ?FileEntity $libreSignFile, |
| 109 | RejectionViewer $viewer, |
| 110 | ): ?array { |
| 111 | $rejectedAt = $signRequest->getRejectedAt(); |
| 112 | if ($signRequest->getStatusEnum() !== SignRequestStatus::REJECTED || $rejectedAt === null) { |
| 113 | return null; |
| 114 | } |
| 115 | |
| 116 | $privileged = $viewer->isPrivilegedFor($signRequest); |
| 117 | $config = $this->rejectionPolicyService->getConfig($libreSignFile); |
| 118 | if (!$privileged && !$config->isRejectionVisibleTo($viewer->getAudience())) { |
| 119 | return null; |
| 120 | } |
| 121 | |
| 122 | $rejection = [ |
| 123 | 'rejectedAt' => $rejectedAt->format(DateTimeInterface::ATOM), |
| 124 | ]; |
| 125 | |
| 126 | $comment = $signRequest->getRejectionComment(); |
| 127 | if ($comment === null || $comment === '') { |
| 128 | return $rejection; |
| 129 | } |
| 130 | |
| 131 | $commentIsPrivate = $signRequest->getRejectionCommentPrivate(); |
| 132 | if ($privileged) { |
| 133 | $rejection['comment'] = $comment; |
| 134 | $rejection['commentPrivate'] = $commentIsPrivate; |
| 135 | return $rejection; |
| 136 | } |
| 137 | |
| 138 | if ($commentIsPrivate || !$config->isCommentVisibleTo($viewer->getAudience())) { |
| 139 | return $rejection; |
| 140 | } |
| 141 | |
| 142 | $rejection['comment'] = $comment; |
| 143 | $rejection['commentPrivate'] = false; |
| 144 | |
| 145 | return $rejection; |
| 146 | } |
| 147 | } |