Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.44% |
76 / 78 |
|
71.43% |
5 / 7 |
CRAP | |
0.00% |
0 / 1 |
| SignRequestService | |
97.44% |
76 / 78 |
|
71.43% |
5 / 7 |
31 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| createOrUpdateSignRequest | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
10 | |||
| getSignRequestByIdentifyMethod | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| assertParticipantRoleCanBeUpdated | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
| populateSignRequest | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
| getDisplayNameFromIdentifyMethodIfEmpty | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
6.05 | |||
| insertOrUpdateSignRequest | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | /** |
| 5 | * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors |
| 6 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 7 | */ |
| 8 | |
| 9 | namespace OCA\Libresign\Service\SignRequest; |
| 10 | |
| 11 | use OCA\Libresign\Db\SignRequest as SignRequestEntity; |
| 12 | use OCA\Libresign\Db\SignRequestMapper; |
| 13 | use OCA\Libresign\Enum\ParticipantRole; |
| 14 | use OCA\Libresign\Enum\SignRequestStatus; |
| 15 | use OCA\Libresign\Exception\LibresignException; |
| 16 | use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; |
| 17 | use OCA\Libresign\Service\IdentifyMethodService; |
| 18 | use OCA\Libresign\Service\NotificationDeliveryService; |
| 19 | use OCP\AppFramework\Db\DoesNotExistException; |
| 20 | use OCP\AppFramework\Db\TTransactional; |
| 21 | use OCP\IDBConnection; |
| 22 | use OCP\IL10N; |
| 23 | use OCP\IUserManager; |
| 24 | use Sabre\DAV\UUIDUtil; |
| 25 | |
| 26 | class SignRequestService { |
| 27 | use TTransactional; |
| 28 | public function __construct( |
| 29 | protected IL10N $l10n, |
| 30 | protected SignRequestMapper $signRequestMapper, |
| 31 | protected IUserManager $userManager, |
| 32 | protected IdentifyMethodService $identifyMethodService, |
| 33 | protected StatusService $signRequestStatusService, |
| 34 | protected IDBConnection $db, |
| 35 | protected NotificationDeliveryService $notifications, |
| 36 | ) { |
| 37 | } |
| 38 | |
| 39 | /** |
| 40 | * Create or update a sign request for a signer |
| 41 | * |
| 42 | * @param array $identifyMethods Identify methods array |
| 43 | * @param string $displayName Signer display name |
| 44 | * @param string|null $description Signer description; null keeps the current value, empty string clears it |
| 45 | * @param bool $notify Whether to notify the signer |
| 46 | * @param int $fileId File ID |
| 47 | * @param int $signingOrder Signing order |
| 48 | * @param int|null $fileStatus File status |
| 49 | * @param int|null $signerStatus Signer status |
| 50 | * @param ParticipantRole $participantRole Participant role (signer or observer) |
| 51 | * @param callable(SignRequestEntity): void|null $afterPersist Callback invoked after the sign request is persisted and before identify methods are saved |
| 52 | * @param string|null $deliveryEventId Shared identity for the explicit request operation, if supplied by its caller |
| 53 | * @return SignRequestEntity |
| 54 | */ |
| 55 | public function createOrUpdateSignRequest( |
| 56 | array $identifyMethods, |
| 57 | string $displayName, |
| 58 | ?string $description, |
| 59 | bool $notify, |
| 60 | int $fileId, |
| 61 | int $signingOrder = 0, |
| 62 | ?int $fileStatus = null, |
| 63 | ?int $signerStatus = null, |
| 64 | ParticipantRole $participantRole = ParticipantRole::SIGNER, |
| 65 | ?callable $afterPersist = null, |
| 66 | ?string $deliveryEventId = null, |
| 67 | ): SignRequestEntity { |
| 68 | $identifyMethodsInstances = $this->identifyMethodService->getByUserData($identifyMethods); |
| 69 | if (empty($identifyMethodsInstances)) { |
| 70 | // TRANSLATORS Error shown when the chosen method to identify the signer (account, email, SMS, etc.) is invalid. |
| 71 | throw new \Exception($this->l10n->t('Invalid identification method')); |
| 72 | } |
| 73 | |
| 74 | $signRequest = $this->getSignRequestByIdentifyMethod( |
| 75 | current($identifyMethodsInstances), |
| 76 | $fileId |
| 77 | ); |
| 78 | |
| 79 | $this->assertParticipantRoleCanBeUpdated($signRequest, $participantRole); |
| 80 | |
| 81 | $displayName = $this->getDisplayNameFromIdentifyMethodIfEmpty($identifyMethodsInstances, $displayName); |
| 82 | $this->populateSignRequest($signRequest, $displayName, $signingOrder, $description, $fileId, $participantRole); |
| 83 | |
| 84 | $isNewSignRequest = !$signRequest->getId(); |
| 85 | $currentStatus = $signRequest->getStatusEnum(); |
| 86 | |
| 87 | if ($isNewSignRequest |
| 88 | || $currentStatus === SignRequestStatus::DRAFT |
| 89 | || $currentStatus === SignRequestStatus::ABLE_TO_SIGN |
| 90 | || $currentStatus === SignRequestStatus::OBSERVING |
| 91 | ) { |
| 92 | $desiredStatus = $this->signRequestStatusService->determineInitialStatus( |
| 93 | $signingOrder, |
| 94 | $fileId, |
| 95 | $fileStatus, |
| 96 | $signerStatus, |
| 97 | $currentStatus, |
| 98 | $participantRole, |
| 99 | ); |
| 100 | $this->signRequestStatusService->updateStatusIfAllowed($signRequest, $currentStatus, $desiredStatus, $isNewSignRequest); |
| 101 | } |
| 102 | |
| 103 | $this->atomic(function () use ($signRequest, $afterPersist, $notify, $fileStatus, $deliveryEventId, $identifyMethodsInstances): void { |
| 104 | $this->insertOrUpdateSignRequest($signRequest); |
| 105 | |
| 106 | if ($afterPersist !== null) { |
| 107 | $afterPersist($signRequest); |
| 108 | } |
| 109 | |
| 110 | $shouldNotify = $notify && $this->signRequestStatusService->shouldNotifySignRequest( |
| 111 | $signRequest->getStatusEnum(), |
| 112 | $fileStatus |
| 113 | ); |
| 114 | |
| 115 | // A new explicit write is a new event even if its visible fields did |
| 116 | // not change. Repeated dispatch of this same event still deduplicates. |
| 117 | // All factors of one request share the same event identity. |
| 118 | $eventId = $shouldNotify |
| 119 | ? ($deliveryEventId ?? 'sign-request-write:' . bin2hex(random_bytes(16))) |
| 120 | : null; |
| 121 | foreach ($identifyMethodsInstances as $identifyMethod) { |
| 122 | $identifyMethod->getEntity()->setSignRequestId($signRequest->getId()); |
| 123 | $identifyMethod->willNotifyUser($shouldNotify); |
| 124 | $identifyMethod->save($eventId); |
| 125 | } |
| 126 | }, $this->db); |
| 127 | $this->notifications->flushCommitted(); |
| 128 | return $signRequest; |
| 129 | } |
| 130 | |
| 131 | public function getSignRequestByIdentifyMethod(IIdentifyMethod $identifyMethod, int $fileId): SignRequestEntity { |
| 132 | try { |
| 133 | $signRequest = $this->signRequestMapper->getByIdentifyMethodAndFileId($identifyMethod, $fileId); |
| 134 | } catch (DoesNotExistException) { |
| 135 | $signRequest = new SignRequestEntity(); |
| 136 | } |
| 137 | return $signRequest; |
| 138 | } |
| 139 | |
| 140 | private function assertParticipantRoleCanBeUpdated( |
| 141 | SignRequestEntity $signRequest, |
| 142 | ParticipantRole $participantRole, |
| 143 | ): void { |
| 144 | if (!$signRequest->getId()) { |
| 145 | return; |
| 146 | } |
| 147 | |
| 148 | if ($signRequest->getStatusEnum() !== SignRequestStatus::SIGNED) { |
| 149 | return; |
| 150 | } |
| 151 | |
| 152 | if ($signRequest->getParticipantRoleEnum() === $participantRole) { |
| 153 | return; |
| 154 | } |
| 155 | |
| 156 | // TRANSLATORS Error shown when trying to change the participant role after the document was already signed. |
| 157 | throw new LibresignException($this->l10n->t('Cannot change the participant role after the document has been signed')); |
| 158 | } |
| 159 | |
| 160 | private function populateSignRequest( |
| 161 | SignRequestEntity $signRequest, |
| 162 | string $displayName, |
| 163 | int $signingOrder, |
| 164 | ?string $description, |
| 165 | int $fileId, |
| 166 | ParticipantRole $participantRole, |
| 167 | ): void { |
| 168 | $signRequest->setFileId($fileId); |
| 169 | $signRequest->setSigningOrder($signingOrder); |
| 170 | $signRequest->setParticipantRoleEnum($participantRole); |
| 171 | if (!$signRequest->getUuid()) { |
| 172 | $signRequest->setUuid(UUIDUtil::getUUID()); |
| 173 | } |
| 174 | if (!empty($displayName)) { |
| 175 | $signRequest->setDisplayName($displayName); |
| 176 | } |
| 177 | if ($description !== null) { |
| 178 | $signRequest->setDescription(trim($description) === '' ? null : $description); |
| 179 | } |
| 180 | if (!$signRequest->getId()) { |
| 181 | $signRequest->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC'))); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * @param IIdentifyMethod[] $identifyMethodsInstances |
| 187 | * @param string $displayName |
| 188 | * @return string |
| 189 | */ |
| 190 | private function getDisplayNameFromIdentifyMethodIfEmpty(array $identifyMethodsInstances, string $displayName): string { |
| 191 | if (!empty($displayName)) { |
| 192 | return $displayName; |
| 193 | } |
| 194 | foreach ($identifyMethodsInstances as $identifyMethod) { |
| 195 | if ($identifyMethod->getName() === 'account') { |
| 196 | return $this->userManager->get($identifyMethod->getEntity()->getIdentifierValue())->getDisplayName(); |
| 197 | } |
| 198 | } |
| 199 | foreach ($identifyMethodsInstances as $identifyMethod) { |
| 200 | if ($identifyMethod->getName() !== 'account') { |
| 201 | return $identifyMethod->getEntity()->getIdentifierValue(); |
| 202 | } |
| 203 | } |
| 204 | return ''; |
| 205 | } |
| 206 | |
| 207 | public function insertOrUpdateSignRequest(SignRequestEntity $signRequest): void { |
| 208 | if ($signRequest->getId()) { |
| 209 | $this->signRequestMapper->update($signRequest); |
| 210 | } else { |
| 211 | $this->signRequestMapper->insert($signRequest); |
| 212 | } |
| 213 | } |
| 214 | } |