Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions lib/Service/File/SigningWorkflowStatusResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Libresign\Service\File;

use OCA\Libresign\Db\File as FileEntity;
use OCA\Libresign\Db\IdDocs;
use OCA\Libresign\Db\IdDocsMapper;
use OCA\Libresign\Db\SignRequest as SignRequestEntity;
use OCA\Libresign\Enum\FileStatus;
use OCA\Libresign\Enum\SignRequestStatus;
use OCA\Libresign\Service\SignatureRejection\SignatureRejectionPolicyService;
use OCP\AppFramework\Db\DoesNotExistException;

/**
* Shared document status resolution for signing and rejection in stable35.
* A rejection only completes a continuing workflow if at least one signature exists.
*/
class SigningWorkflowStatusResolver {
public function __construct(
private IdDocsMapper $idDocsMapper,
private SignatureRejectionPolicyService $rejectionPolicyService,
) {
}

/**
* @param SignRequestEntity[] $signRequests
*/
public function resolve(FileEntity $file, array $signRequests): ?FileStatus {
$signers = $this->excludeIdDocUploaderPlaceholder($file, $signRequests);
$total = count($signers);
if ($total === 0) {
return null;
}

$totalSigned = count(array_filter($signers, static fn (SignRequestEntity $signer): bool => $signer->getSigned() !== null));
if ($totalSigned === $total) {
return FileStatus::SIGNED;
}

if ($totalSigned > 0) {
$policy = $this->rejectionPolicyService->getPolicyValue($file);
if ($policy['enabled'] && !$policy['cancel_workflow']) {
$totalRejected = count(array_filter(
$signers,
static fn (SignRequestEntity $signer): bool => $signer->getStatusEnum() === SignRequestStatus::REJECTED,
));
if ($totalSigned + $totalRejected === $total) {
return FileStatus::SIGNED;
}
}
return FileStatus::PARTIAL_SIGNED;
}

return null;
}

/**
* @param SignRequestEntity[] $signRequests
* @return SignRequestEntity[]
*/
private function excludeIdDocUploaderPlaceholder(FileEntity $file, array $signRequests): array {
$fileId = $file->getId();
if ($fileId === null) {
return $signRequests;
}
try {
$idDocs = $this->idDocsMapper->getByFileId($fileId);
} catch (DoesNotExistException) {
return $signRequests;
}
if (!$idDocs instanceof IdDocs) {
return $signRequests;
}
$placeholderId = $idDocs->getSignRequestId();
return array_values(array_filter(
$signRequests,
static fn (SignRequestEntity $signer): bool => $signer->getId() !== $placeholderId,
));
}
}
61 changes: 5 additions & 56 deletions lib/Service/SignFileService.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use OCA\Libresign\Helper\JSActions;
use OCA\Libresign\Helper\ValidateHelper;
use OCA\Libresign\Service\Envelope\EnvelopeStatusDeterminer;
use OCA\Libresign\Service\File\SigningWorkflowStatusResolver;
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\IToken;
use OCA\Libresign\Service\Policy\PolicyService;
Expand Down Expand Up @@ -129,6 +130,7 @@ public function __construct(
private SignRequestService $signRequestService,
private PolicyService $policyService,
private TemporarySignatureImageService $temporarySignatureImageService,
private SigningWorkflowStatusResolver $signingWorkflowStatusResolver,
) {
}

Expand Down Expand Up @@ -1092,7 +1094,7 @@ protected function getSigners(): array {
}

protected function setNewStatusIfNecessary(FileEntity $libreSignFile): bool {
$newStatus = $this->evaluateStatusFromSigners();
$newStatus = $this->evaluateStatusFromSigners($libreSignFile);

if ($newStatus === null || $newStatus === $libreSignFile->getStatus()) {
return false;
Expand All @@ -1107,61 +1109,8 @@ private function updateEntityCacheAfterDbSave(FileEntity $file): void {
$this->statusService->cacheFileStatus($file);
}

private function evaluateStatusFromSigners(): ?int {
$signers = $this->excludeIdDocUploaderPlaceholder($this->getSigners());

$total = count($signers);

if ($total === 0) {
return null;
}

$totalSigned = count(array_filter($signers, fn ($s) => $s->getSigned() !== null));

if ($totalSigned === $total) {
return FileStatus::SIGNED->value;
}

if ($totalSigned > 0) {
return FileStatus::PARTIAL_SIGNED->value;
}

return null;
}

/**
* @param SignRequestEntity[] $signers
* @return SignRequestEntity[]
*/
private function excludeIdDocUploaderPlaceholder(array $signers): array {
$placeholderId = $this->getIdDocUploaderSignRequestId();
if ($placeholderId === null) {
return $signers;
}

return array_values(array_filter(
$signers,
static fn (SignRequestEntity $signer): bool => $signer->getId() !== $placeholderId,
));
}

private function getIdDocUploaderSignRequestId(): ?int {
$fileId = $this->signRequest->getFileId();
if ($fileId === null) {
return null;
}

try {
$idDocs = $this->idDocsMapper->getByFileId($fileId);
} catch (DoesNotExistException) {
return null;
}

if (!$idDocs instanceof IdDocs) {
return null;
}

return $idDocs->getSignRequestId();
private function evaluateStatusFromSigners(FileEntity $file): ?int {
return $this->signingWorkflowStatusResolver->resolve($file, $this->getSigners())?->value;
}

private function getOrGeneratePfxContent(SignEngineHandler $engine): string {
Expand Down
64 changes: 64 additions & 0 deletions lib/Service/SignatureRejection/SignatureRejectionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use OCA\Libresign\Enum\SignRequestStatus;
use OCA\Libresign\Events\SignatureRejectedEvent;
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Service\File\SigningWorkflowStatusResolver;
use OCA\Libresign\Service\FileStatusService;
use OCA\Libresign\Service\IdentifyMethodService;
use OCP\AppFramework\Utility\ITimeFactory;
Expand Down Expand Up @@ -46,6 +47,7 @@ public function __construct(
private ITimeFactory $timeFactory,
private IL10N $l10n,
private LoggerInterface $logger,
private SigningWorkflowStatusResolver $statusResolver,
) {
}

Expand Down Expand Up @@ -107,6 +109,7 @@ private function persistRejection(
);
$previousFileStatus = $libreSignFile->getStatus();
$rejectedAt = $this->timeFactory->getDateTime();
$previousDocumentStates = [];

$this->db->beginTransaction();
try {
Expand All @@ -120,12 +123,20 @@ private function persistRejection(

if ($workflowCanceled) {
$this->cancelWorkflow($libreSignFile);
} else {
$this->resolveStatusOfRejectedDocuments($libreSignFile, $signRequests, $previousDocumentStates);
}

$this->db->commit();
} catch (\Throwable $e) {
$this->db->rollBack();
$this->restoreState($libreSignFile, $signRequests, $previousSignerStatuses, $previousFileStatus);
foreach ($previousDocumentStates as $entry) {
$document = $entry['file'];
$document->setStatus($entry['status']);
$document->setMetadata($entry['metadata']);
$this->fileStatusService->invalidateCachedStatus($document);
}
$this->logger->error('Failed to register the signature rejection: ' . $e->getMessage(), [
'exception' => $e,
'signRequestId' => $signRequest->getId(),
Expand All @@ -136,6 +147,59 @@ private function persistRejection(
}
}

/**
* Resolve the documents affected by this rejection in the same transaction.
* An envelope's final outcome is derived separately from its documents.
*
* @param list<SignRequestEntity> $signRequests
* @param array<int, array{file: FileEntity, status: int, metadata: ?array}> $previousDocumentStates
*/
private function resolveStatusOfRejectedDocuments(
FileEntity $libreSignFile,
array $signRequests,
array &$previousDocumentStates,
): void {
foreach ($signRequests as $rejected) {
$fileId = $rejected->getFileId();
if ($fileId === null || isset($previousDocumentStates[$fileId])) {
continue;
}
$document = $fileId === $libreSignFile->getId()
? $libreSignFile
: $this->fileMapper->getById($fileId);
if ($document->isEnvelope()) {
continue;
}
$status = $this->statusResolver->resolve(
$document,
$this->signRequestMapper->getByFileId($fileId),
);
if ($status === null || $status->value === $document->getStatus()) {
continue;
}
$previousDocumentStates[$fileId] = [
'file' => $document,
'status' => $document->getStatus(),
'metadata' => $document->getMetadata(),
];
if ($document->hasParent()) {
$parentId = $document->getParentFileId();
if ($parentId !== null && !isset($previousDocumentStates[$parentId])) {
$parent = $libreSignFile->getId() === $parentId
? $libreSignFile
: $this->fileMapper->getById($parentId);
$previousDocumentStates[$parentId] = [
'file' => $parent,
'status' => $parent->getStatus(),
'metadata' => $parent->getMetadata(),
];
}
}
$document->setStatusEnum($status);
$this->fileStatusService->update($document);
}
}

/**
* A signer of an envelope has one signature request per document it contains,
* plus one on the envelope itself. Signing closes all of them, so a rejection
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"jarnaiz/behat-junit-formatter": "^1.3",
"php-http/guzzle7-adapter": "^1.1",
"php-http/message": "^1.16",
"libresign/nextcloud-behat": "^1.6",
"libresign/nextcloud-behat": "^1.6.4",
"libresign/mailpit-behat-extension": "^0.1.1",
"libresign/behat-tsa-extension": "^0.1.0"
},
Expand Down
Loading
Loading