Skip to content
Open
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
49 changes: 49 additions & 0 deletions lib/Enum/SignerDisplayStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Libresign\Enum;

use OCP\IL10N;

/**
* What the current viewer is allowed to know about a signer. It is an API
* presentation value only: SignRequestStatus stays the workflow state and
* nothing here is persisted or used for authorization.
*/
enum SignerDisplayStatus: string {
case DRAFT = 'draft';
case READY_TO_SIGN = 'ready_to_sign';
case SIGNED = 'signed';
case REJECTED = 'rejected';
case OBSERVING = 'observing';
/** The signer has not signed and the viewer may not know anything more specific. */
case NOT_SIGNED = 'not_signed';

public static function fromSignRequestStatus(SignRequestStatus $status): self {
return match ($status) {
SignRequestStatus::DRAFT => self::DRAFT,
SignRequestStatus::ABLE_TO_SIGN => self::READY_TO_SIGN,
SignRequestStatus::SIGNED => self::SIGNED,
SignRequestStatus::REJECTED => self::REJECTED,
SignRequestStatus::OBSERVING => self::OBSERVING,
};
}

public function getLabel(IL10N $l10n): string {
return match ($this) {
self::DRAFT => SignRequestStatus::DRAFT->getLabel($l10n),
self::READY_TO_SIGN => SignRequestStatus::ABLE_TO_SIGN->getLabel($l10n),
self::SIGNED => SignRequestStatus::SIGNED->getLabel($l10n),
self::REJECTED => SignRequestStatus::REJECTED->getLabel($l10n),
self::OBSERVING => SignRequestStatus::OBSERVING->getLabel($l10n),
// TRANSLATORS Neutral signer status shown when the viewer may know only that this signer has not signed, without the reason or whether they still can.
self::NOT_SIGNED => $l10n->t('Not signed'),
};
}
}
4 changes: 3 additions & 1 deletion lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,15 @@
* notify?: LibresignNotify[],
* certificate_info?: LibresignSignerCertificateInfo,
* }
* @psalm-type LibresignSignerDisplayStatus = 'draft'|'ready_to_sign'|'signed'|'rejected'|'observing'|'not_signed'
* @psalm-type LibresignSignerSummary = array{
* signRequestId: int,
* displayName: string,
* email?: ?string,
* identifyMethods?: LibresignIdentifyMethod[],
* signed: ?string,
* status: 0|1|2|3|4,
* displayStatus: LibresignSignerDisplayStatus,
* status?: 0|1|2|3|4,
* statusText: string,
* participantRole?: LibresignParticipantRole,
* }
Expand Down
21 changes: 19 additions & 2 deletions lib/Service/File/EnvelopeAssembler.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OCA\Libresign\Service\FileElementService;
use OCA\Libresign\Service\FolderService;
use OCA\Libresign\Service\IdentifyMethodService;
use OCA\Libresign\Service\SignatureRejection\SignatureRejectionVisibilityService;
use OCP\IURLGenerator;
use Psr\Log\LoggerInterface;

Expand All @@ -31,6 +32,7 @@ public function __construct(
private \OCA\Libresign\Handler\SignEngine\Pkcs12Handler $pkcs12Handler,
private LoggerInterface $logger,
private FileElementService $fileElementService,
private SignatureRejectionVisibilityService $signatureRejectionVisibilityService,
) {
}

Expand Down Expand Up @@ -74,6 +76,17 @@ public function buildEnvelopeChildData(File $childFile, \OCA\Libresign\Service\F
->setIsRequest(false)
->getIdentifyMethodsFromSignRequestIds($signRequestIds);

// A child document follows the same rejection visibility rules as a
// single file: the viewer must be known for every signer first.
$isRequester = $options->getMe() !== null && $options->getMe()->getUID() === $childFile->getUserId();
$viewerSignRequestIds = [];
foreach ($signRequests as $signRequest) {
if ($isRequester || $options->isViewerOfSigner($identifyMethodsBatch[$signRequest->getId()] ?? [])) {
$viewerSignRequestIds[] = $signRequest->getId();
}
}
$hiddenRejection = $this->signatureRejectionVisibilityService->hasHiddenRejection($childFile, $signRequests, $viewerSignRequestIds);

foreach ($signRequests as $signRequest) {
$identifyMethods = $identifyMethodsBatch[$signRequest->getId()] ?? [];
$identifyMethodsArray = [];
Expand Down Expand Up @@ -115,8 +128,12 @@ public function buildEnvelopeChildData(File $childFile, \OCA\Libresign\Service\F
$signer->email = $email;
$signer->uid = $signerUid;
$signer->signed = $signed;
$signer->status = $signRequest->getStatus();
$signer->statusText = $this->signRequestMapper->getTextOfSignerStatus($signRequest->getStatus());
$this->signatureRejectionVisibilityService->presentSigner(
$signRequest,
$childFile,
in_array($signRequest->getId(), $viewerSignRequestIds, true),
$hiddenRejection,
)->applyToObject($signer);
$signer->identifyMethods = $identifyMethodsArray;
$signer->metadata = $signRequest->getMetadata();
$signer->visibleElements = [];
Expand Down
121 changes: 73 additions & 48 deletions lib/Service/File/FileListService.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,17 +267,16 @@ private function formatSingleFileData(
$file['files'] = [];
} else {
$file['filesCount'] = 1;
$file['files'] = $this->formatChildFilesResponse([$fileEntity], $signers, $identifyMethods);
$file['files'] = $this->formatChildFilesResponse([$fileEntity], $signers, $identifyMethods, $user, $meSignRequestId);
}

// Remove raw fields not needed in response
unset($file['userId'], $file['createdAt']);

$file['signers'] = [];
foreach ($signers as $signer) {
if ($signer->getFileId() !== $fileEntity->getId()) {
continue;
}
$signersOfFile = array_values(array_filter($signers, static fn (SignRequest $signer): bool => $signer->getFileId() === $fileEntity->getId()));
$hiddenRejection = $this->hasHiddenRejection($fileEntity, $signersOfFile, $identifyMethods, $user, $meSignRequestId);
foreach ($signersOfFile as $signer) {
$signerData = $this->formatSignerData(
$signer,
$identifyMethods,
Expand All @@ -286,6 +285,7 @@ private function formatSingleFileData(
$user,
$meSignRequestId,
$fileEntity,
$hiddenRejection,
);
$file['signers'][] = $signerData;
if (!empty($signerData['me']) && isset($signerData['sign_request_uuid']) && !isset($file['url'])) {
Expand Down Expand Up @@ -400,23 +400,11 @@ private function formatSignerData(
?IUser $user,
?int $meSignRequestId = null,
?File $fileEntity = null,
bool $hiddenRejection = false,
): array {
$identifyMethodsOfSigner = $identifyMethods[$signer->getId()] ?? [];
$resolvedDisplayName = $this->resolveSignerDisplayName($signer, $identifyMethodsOfSigner);
$me = false;
if ($meSignRequestId !== null) {
$me = $signer->getId() === $meSignRequestId;
} elseif ($user) {
$me = array_reduce($identifyMethodsOfSigner, function (bool $carry, IdentifyMethod $identifyMethod) use ($user): bool {
if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT) {
return $user->getUID() === $identifyMethod->getIdentifierValue();
}
if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL && $user->getEMailAddress()) {
return $user->getEMailAddress() === $identifyMethod->getIdentifierValue();
}
return $carry;
}, false);
}
$me = $this->isSignerTheViewer($signer, $identifyMethodsOfSigner, $user, $meSignRequestId);
/** @var LibresignSignerDetail */
$data = [
'email' => array_reduce($identifyMethodsOfSigner, function (string $carry, IdentifyMethod $identifyMethod): string {
Expand All @@ -434,8 +422,6 @@ private function formatSignerData(
'signed' => null,
'signRequestId' => $signer->getId(),
'signingOrder' => $signer->getSigningOrder(),
'status' => $signer->getStatus(),
'statusText' => $this->signRequestMapper->getTextOfSignerStatus($signer->getStatus()),
'participantRole' => $signer->getParticipantRoleEnum()->value,
'me' => $me,
'visibleElements' => isset($visibleElements[$signer->getId()])
Expand Down Expand Up @@ -487,20 +473,62 @@ private function formatSignerData(
$data['metadata'] = $geolocationMetadata;
}

$requesterUserId = $fileEntity?->getUserId() ?? '';
$rejection = $this->signatureRejectionVisibilityService->buildSignerRejection(
/** @var LibresignSignerDetail $data */
$data = $this->signatureRejectionVisibilityService->presentSigner(
$signer,
$fileEntity,
$data['me'] || ($requesterUserId !== '' && $user?->getUID() === $requesterUserId),
);
if ($rejection !== null) {
$data['rejection'] = $rejection;
}
$data['me'] || $this->isRequester($fileEntity, $user),
$hiddenRejection,
)->applyTo($data);

ksort($data);
return $data;
}

private function isRequester(?File $fileEntity, ?IUser $user): bool {
$requesterUserId = $fileEntity?->getUserId() ?? '';
return $requesterUserId !== '' && $user?->getUID() === $requesterUserId;
}

/**
* @param array<string, IdentifyMethod> $identifyMethodsOfSigner
*/
private function isSignerTheViewer(SignRequest $signer, array $identifyMethodsOfSigner, ?IUser $user, ?int $meSignRequestId): bool {
if ($meSignRequestId !== null) {
return $signer->getId() === $meSignRequestId;
}
if (!$user) {
return false;
}
return array_reduce($identifyMethodsOfSigner, function (bool $carry, IdentifyMethod $identifyMethod) use ($user): bool {
if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT) {
return $user->getUID() === $identifyMethod->getIdentifierValue();
}
if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL && $user->getEMailAddress()) {
return $user->getEMailAddress() === $identifyMethod->getIdentifierValue();
}
return $carry;
}, false);
}

/**
* Whether the file holds a rejection this viewer may not know about; the
* requester and the signers the viewer is are privileged.
*
* @param SignRequest[] $signersOfFile
* @param array<int, array<string, IdentifyMethod>> $identifyMethods
*/
private function hasHiddenRejection(?File $fileEntity, array $signersOfFile, array $identifyMethods, ?IUser $user, ?int $meSignRequestId): bool {
$requester = $this->isRequester($fileEntity, $user);
$privileged = [];
foreach ($signersOfFile as $signer) {
if ($requester || $this->isSignerTheViewer($signer, $identifyMethods[$signer->getId()] ?? [], $user, $meSignRequestId)) {
$privileged[] = $signer->getId();
}
}
return $this->signatureRejectionVisibilityService->hasHiddenRejection($fileEntity, $signersOfFile, $privileged);
}

/**
* @psalm-return array{
* geolocationRequirement?: LibresignGeolocationRequirement,
Expand Down Expand Up @@ -561,6 +589,7 @@ private function formatSignerDataBasic(
array $identifyMethods,
array $visibleElements,
?File $fileEntity = null,
bool $hiddenRejection = false,
): array {
$identifyMethodsOfSigner = $identifyMethods[$signer->getId()] ?? [];
$resolvedDisplayName = $this->resolveSignerDisplayName($signer, $identifyMethodsOfSigner);
Expand All @@ -581,8 +610,6 @@ private function formatSignerDataBasic(
'signed' => null,
'signRequestId' => $signer->getId(),
'signingOrder' => $signer->getSigningOrder(),
'status' => $signer->getStatus(),
'statusText' => $this->signRequestMapper->getTextOfSignerStatus($signer->getStatus()),
'participantRole' => $signer->getParticipantRoleEnum()->value,
'me' => false,
'visibleElements' => isset($visibleElements[$signer->getId()])
Expand All @@ -602,14 +629,8 @@ private function formatSignerDataBasic(
$data['signed'] = $signer->getSigned()->format(DateTimeInterface::ATOM);
}

$rejection = $this->signatureRejectionVisibilityService->buildSignerRejection(
$signer,
$fileEntity,
false,
);
if ($rejection !== null) {
$data['rejection'] = $rejection;
}
/** @var LibresignSignerDetail $data */
$data = $this->signatureRejectionVisibilityService->presentSigner($signer, $fileEntity, false, $hiddenRejection)->applyTo($data);

ksort($data);
return $data;
Expand Down Expand Up @@ -705,16 +726,17 @@ public function formatFileWithChildren(File $mainEntity, array $childFiles, ?IUs

$signers = [];
$currentSignerRequestUuid = null;
$hiddenRejection = $this->hasHiddenRejection($mainEntity, $signRequestEntities, $identifyMethods, $user, null);
foreach ($signRequestEntities as $signer) {
if ($user) {
$signerData = $this->formatSignerData($signer, $identifyMethods, $visibleElementsData, $metadata, $user, null, $mainEntity);
$signerData = $this->formatSignerData($signer, $identifyMethods, $visibleElementsData, $metadata, $user, null, $mainEntity, $hiddenRejection);
$signers[] = $signerData;

if ($currentSignerRequestUuid === null && !empty($signerData['me']) && isset($signerData['sign_request_uuid'])) {
$currentSignerRequestUuid = $signerData['sign_request_uuid'];
}
} else {
$signers[] = $this->formatSignerDataBasic($signer, $identifyMethods, $visibleElementsData, $mainEntity);
$signers[] = $this->formatSignerDataBasic($signer, $identifyMethods, $visibleElementsData, $mainEntity, $hiddenRejection);
}
}

Expand Down Expand Up @@ -768,14 +790,15 @@ public function formatFileWithChildren(File $mainEntity, array $childFiles, ?IUs
$childFiles,
$childContext['signers'] ?? null,
$childContext['identifyMethods'] ?? null,
$user,
);
$response['size'] = array_sum(array_map(
static fn (array $file): int => (int)$file['size'],
$response['files'],
));
} else {
$response['filesCount'] = 1;
$response['files'] = $this->formatChildFilesResponse([$mainEntity], $signRequestEntities, $identifyMethods);
$response['files'] = $this->formatChildFilesResponse([$mainEntity], $signRequestEntities, $identifyMethods, $user);
$response['size'] = (int)$response['files'][0]['size'];
}

Expand Down Expand Up @@ -934,6 +957,8 @@ private function formatChildFilesResponse(
array $files,
?array $allSigners = null,
?array $identifyMethods = null,
?IUser $user = null,
?int $meSignRequestId = null,
): array {
$fileIds = array_map(fn (File $file) => $file->getId(), $files);
$allSigners ??= $fileIds ? $this->signRequestMapper->getByMultipleFileId($fileIds) : [];
Expand All @@ -944,11 +969,12 @@ private function formatChildFilesResponse(
$signersByFileId[$signer->getFileId()][] = $signer;
}

return array_values(array_map(function (File $file) use ($signersByFileId, $identifyMethods) {
return array_values(array_map(function (File $file) use ($signersByFileId, $identifyMethods, $user, $meSignRequestId) {
$signers = $signersByFileId[$file->getId()] ?? [];
$metadata = $file->getMetadata() ?? [];
$size = $this->getFileSize($file);
$signersFormatted = array_map(function (SignRequest $signer) use ($identifyMethods) {
$hiddenRejection = $this->hasHiddenRejection($file, $signers, $identifyMethods, $user, $meSignRequestId);
$signersFormatted = array_map(function (SignRequest $signer) use ($identifyMethods, $file, $user, $meSignRequestId, $hiddenRejection) {
$identifyMethodsOfSigner = $identifyMethods[$signer->getId()] ?? [];
$email = array_reduce($identifyMethodsOfSigner, function (string $carry, IdentifyMethod $identifyMethod): string {
if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL) {
Expand All @@ -966,8 +992,10 @@ private function formatChildFilesResponse(
return $carry;
}, $signer->getDisplayName());

$privileged = $this->isRequester($file, $user)
|| $this->isSignerTheViewer($signer, $identifyMethodsOfSigner, $user, $meSignRequestId);
/** @var LibresignSignerSummary */
return [
return $this->signatureRejectionVisibilityService->presentSigner($signer, $file, $privileged, $hiddenRejection)->applyTo([
'signRequestId' => $signer->getId(),
'displayName' => $displayName,
'email' => $email,
Expand All @@ -977,10 +1005,7 @@ private function formatChildFilesResponse(
'requirement' => $identifyMethod->getRequirement(),
], array_values($identifyMethodsOfSigner)),
'signed' => $signer->getSigned()?->format(\DateTimeInterface::ATOM),
'status' => $signer->getSigned() ? 1 : 0,
// TRANSLATORS Signer status labels on a document list: "Signed" when the person finished signing, "Pending" when their signature is still awaited.
'statusText' => $signer->getSigned() ? $this->l10n->t('Signed') : $this->l10n->t('Pending'),
];
]);
}, $signers);

return [
Expand Down
Loading
Loading