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
2 changes: 2 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@
* metadata: LibresignFileRuntimeMetadata,
* docmdpLevel: int,
* signatureFlow: 'none'|'parallel'|'ordered_numeric',
* policySnapshotFrozen: bool,
* signersCount: int,
* signers: list<LibresignSignerSummary>,
* requested_by: LibresignRequestedBy,
Expand Down Expand Up @@ -791,6 +792,7 @@
* size: non-negative-int,
* docmdpLevel: int,
* signatureFlow: 'none'|'parallel'|'ordered_numeric',
* policySnapshotFrozen: bool,
* visibleElements: LibresignVisibleElement[],
* signers: LibresignSignerDetail[],
* signersCount: int,
Expand Down
62 changes: 57 additions & 5 deletions lib/Service/File/FileListService.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use OCA\Libresign\Enum\SignRequestStatus;
use OCA\Libresign\ResponseDefinitions;
use OCA\Libresign\Service\FileElementService;
use OCA\Libresign\Service\FileStatusService;
use OCA\Libresign\Service\FolderService;
use OCA\Libresign\Service\IdentifyMethodService;
use OCA\Libresign\Service\SignatureRejection\RejectionViewer;
Expand Down Expand Up @@ -94,11 +95,12 @@ public function listAssociatedFilesOfSignFlow(

$signers = $this->signRequestMapper->getByMultipleFileId(array_map(fn (File $file) => $file->getId(), $return['data']));
$identifyMethods = $this->signRequestMapper->getIdentifyMethodsFromSigners($signers);
$envelopes = $this->getEnvelopesByFileId($return['data']);
if ($details) {
$visibleElements = $this->signRequestMapper->getVisibleElementsFromSigners($signers);
$return['data'] = $this->associateAllAndFormat($user, $return['data'], $signers, $identifyMethods, $visibleElements);
$return['data'] = $this->associateAllAndFormat($user, $return['data'], $signers, $identifyMethods, $visibleElements, $envelopes);
} else {
$return['data'] = $this->associateAllAndFormatSummary($user, $return['data'], $signers, $identifyMethods);
$return['data'] = $this->associateAllAndFormatSummary($user, $return['data'], $signers, $identifyMethods, $envelopes);
}

$return['pagination']->setRouteName('ocs.libresign.File.list');
Expand All @@ -112,18 +114,20 @@ public function listAssociatedFilesOfSignFlow(
* @param File[] $files
* @param SignRequest[] $signers
* @param array<int, array<string, Entity&IdentifyMethod>> $identifyMethods
* @param array<int, File> $envelopes
* @return list<LibresignFileSummary>
*/
private function associateAllAndFormatSummary(
IUser $user,
array $files,
array $signers,
array $identifyMethods,
array $envelopes,
): array {
$formattedFiles = [];
foreach ($files as $file) {
$fileSigners = array_filter($signers, fn ($signer) => $signer->getFileId() === $file->getId());
$formattedFiles[] = $this->formatSingleFileSummary($file, $fileSigners, $identifyMethods, $user);
$formattedFiles[] = $this->formatSingleFileSummary($file, $fileSigners, $identifyMethods, $user, $envelopes[$file->getId()] ?? null);
}
return $formattedFiles;
}
Expand All @@ -132,8 +136,9 @@ public function formatSingleFile(IUser $user, File $file): array {
$signers = $this->signRequestMapper->getByMultipleFileId([$file->getId()]);
$identifyMethods = $this->signRequestMapper->getIdentifyMethodsFromSigners($signers);
$visibleElements = $this->signRequestMapper->getVisibleElementsFromSigners($signers);
$envelope = $this->getEnvelopesByFileId([$file])[$file->getId()] ?? null;

return $this->formatSingleFileData($file, $signers, $identifyMethods, $visibleElements, $user);
return $this->formatSingleFileData($file, $signers, $identifyMethods, $visibleElements, $user, null, $envelope);
}

public function formatSingleFileForSignRequest(File $file, ?SignRequest $currentSignRequest = null): array {
Expand Down Expand Up @@ -212,6 +217,7 @@ public function formatEnvelopeChildFilesForSignRequest(
* @param SignRequest[] $signers
* @param array<int, array<string, Entity&IdentifyMethod>> $identifyMethods
* @param array<int, FileElement[]> $visibleElements
* @param array<int, File> $envelopes
* @return list<LibresignDetailedFile>
*/
private function associateAllAndFormat(
Expand All @@ -220,11 +226,12 @@ private function associateAllAndFormat(
array $signers,
array $identifyMethods,
array $visibleElements,
array $envelopes,
): array {
$formattedFiles = [];
foreach ($files as $file) {
$fileSigners = array_filter($signers, fn ($signer) => $signer->getFileId() === $file->getId());
$formattedFiles[] = $this->formatSingleFileData($file, $fileSigners, $identifyMethods, $visibleElements, $user);
$formattedFiles[] = $this->formatSingleFileData($file, $fileSigners, $identifyMethods, $visibleElements, $user, null, $envelopes[$file->getId()] ?? null);
}
return $formattedFiles;
}
Expand All @@ -247,6 +254,7 @@ private function formatSingleFileData(
array $visibleElements,
?IUser $user,
?int $meSignRequestId = null,
?File $envelope = null,
): array {
$file = [
'id' => $fileEntity->getId(),
Expand All @@ -261,6 +269,10 @@ private function formatSingleFileData(
'signatureFlow' => $fileEntity->getSignatureFlow(),
'nodeType' => $fileEntity->getNodeType(),
];
// The sign page formats files without a user and may be public.
if ($user !== null) {
$file['policySnapshotFrozen'] = FileStatusService::isPolicySnapshotFrozen($fileEntity, $envelope);
}
$file['signatureFlow'] = SignatureFlow::fromNumeric($file['signatureFlow'])->value;
$file['statusText'] = $this->fileMapper->getTextOfStatus($file['status']);
$file['requested_by'] = [
Expand Down Expand Up @@ -339,6 +351,7 @@ private function formatSingleFileSummary(
array $signers,
array $identifyMethods,
IUser $user,
?File $envelope = null,
): array {
$metadata = $fileEntity->getMetadata() ?? [];
$nodeType = $fileEntity->getNodeType();
Expand Down Expand Up @@ -384,6 +397,7 @@ private function formatSingleFileSummary(
'metadata' => $metadata,
'docmdpLevel' => $fileEntity->getDocmdpLevel(),
'signatureFlow' => SignatureFlow::fromNumeric($fileEntity->getSignatureFlow())->value,
'policySnapshotFrozen' => FileStatusService::isPolicySnapshotFrozen($fileEntity, $envelope),
'signersCount' => count($signers),
'signers' => [],
'requested_by' => [
Expand Down Expand Up @@ -506,6 +520,40 @@ private function formatSignerData(
return $data;
}

/**
* The envelope of each file that has one, by file id. Envelopes already
* among the files are reused and every other one is loaded once.
*
* @param File[] $files
* @return array<int, File>
*/
private function getEnvelopesByFileId(array $files): array {
$loaded = [];
foreach ($files as $file) {
$loaded[$file->getId()] = $file;
}

$envelopes = [];
foreach ($files as $file) {
$parentId = $file->getParentFileId();
if ($parentId === null) {
continue;
}
if (!array_key_exists($parentId, $loaded)) {
try {
$loaded[$parentId] = $this->fileMapper->getById($parentId);
} catch (DoesNotExistException) {
$loaded[$parentId] = null;
}
}
if ($loaded[$parentId] !== null) {
$envelopes[$file->getId()] = $loaded[$parentId];
}
}

return $envelopes;
}

private function isRequester(?File $fileEntity, ?IUser $user): bool {
$requesterUserId = $fileEntity?->getUserId() ?? '';
return $requesterUserId !== '' && $user?->getUID() === $requesterUserId;
Expand Down Expand Up @@ -830,6 +878,10 @@ public function formatFileWithChildren(File $mainEntity, array $childFiles, ?IUs
'metadata' => $metadata,
'docmdpLevel' => $mainEntity->getDocmdpLevel(),
'signatureFlow' => SignatureFlow::fromNumeric($mainEntity->getSignatureFlow())->value,
'policySnapshotFrozen' => FileStatusService::isPolicySnapshotFrozen(
$mainEntity,
$this->getEnvelopesByFileId([$mainEntity])[$mainEntity->getId()] ?? null,
),
'signers' => $signers,
'signersCount' => count($signers),
'requested_by' => [
Expand Down
18 changes: 18 additions & 0 deletions lib/Service/FileStatusService.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,24 @@ public function markPolicySnapshotFrozen(FileEntity $file, int $newStatus): void
$file->setMetadata($metadata);
}

/**
* A document freezes with its envelope, so pass the envelope of a document
* that has one.
*/
public static function isPolicySnapshotFrozen(FileEntity $file, ?FileEntity $envelope = null): bool {
return self::hasFrozenPolicySnapshot($file)
|| ($envelope !== null && self::hasFrozenPolicySnapshot($envelope));
}

/**
* A request sent before the freeze was recorded has no marker, so it is
* frozen while its status says it was sent.
*/
private static function hasFrozenPolicySnapshot(FileEntity $file): bool {
return isset(($file->getMetadata() ?? [])[self::POLICY_SNAPSHOT_FROZEN_AT])
|| $file->getStatus() >= FileStatus::ABLE_TO_SIGN->value;
}

private function touchStatusChangedAt(FileEntity $file): void {
$metadata = $file->getMetadata() ?? [];
$metadata['status_changed_at'] = (new DateTime())->format(DateTimeInterface::ATOM);
Expand Down
13 changes: 1 addition & 12 deletions lib/Service/Policy/AbstractFilePolicyApplier.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

use OCA\Libresign\Db\File as FileEntity;
use OCA\Libresign\Db\FileMapper;
use OCA\Libresign\Enum\FileStatus;
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Service\FileService;
use OCA\Libresign\Service\FileStatusService;
Expand Down Expand Up @@ -97,17 +96,7 @@ protected function isPolicySnapshotFrozen(FileEntity $file, string $policyKey, s
}
}

$envelope = $this->findEnvelope($file);
if ($this->hasFrozenMarker($file) || $this->hasFrozenMarker($envelope)) {
return true;
}

return $file->getStatus() >= FileStatus::ABLE_TO_SIGN->value
|| ($envelope !== null && $envelope->getStatus() >= FileStatus::ABLE_TO_SIGN->value);
}

private function hasFrozenMarker(?FileEntity $file): bool {
return isset(($file?->getMetadata() ?? [])[FileStatusService::POLICY_SNAPSHOT_FROZEN_AT]);
return FileStatusService::isPolicySnapshotFrozen($file, $this->findEnvelope($file));
}

private function findEnvelope(FileEntity $file): ?FileEntity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@

use OCA\Libresign\Db\File as FileEntity;
use OCA\Libresign\Enum\SignatureFlow;
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Service\Policy\AbstractFilePolicyApplier;
use OCA\Libresign\Service\Policy\Model\ResolvedPolicy;
use OCA\Libresign\Service\Policy\Provider\Signature\SignatureFlowPolicy;
use OCP\AppFramework\Http;
use OCP\IUser;

class SignatureFlowFilePolicyApplier extends AbstractFilePolicyApplier {
Expand All @@ -33,6 +35,11 @@ public function apply(FileEntity $file, array $data): void {
#[\Override]
public function sync(FileEntity $file, array $data): void {
$requestOverrides = $this->getOverrides($data);
if ($this->isPolicySnapshotFrozen($file, SignatureFlowPolicy::KEY)) {
$this->assertFrozenFlowIsKept($file, $requestOverrides);
return;
}

$activeContext = $this->extractActiveContext($data);
$resolvedPolicy = $activeContext === null
? $this->policyService->resolveForUserId(SignatureFlowPolicy::KEY, $file->getUserId(), $requestOverrides)
Expand Down Expand Up @@ -63,4 +70,23 @@ private function getOverrides(array $data): array {
private function assertOverrideAllowed(array $requestOverrides, ResolvedPolicy $resolvedPolicy): void {
$this->assertRequestOverrideAllowed($requestOverrides, $resolvedPolicy, 'Signature flow override is blocked by %s.');
}

/**
* A frozen request keeps the flow it was sent with. A client may still send
* that same flow along an unrelated update, which changes nothing.
*
* @param array<string, string> $requestOverrides
*/
private function assertFrozenFlowIsKept(FileEntity $file, array $requestOverrides): void {
if ($requestOverrides === [] || $requestOverrides[SignatureFlowPolicy::KEY] === $file->getSignatureFlowEnum()->value) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: on a frozen request, signature_flow: null gets "cannot be changed", while the non-frozen path treats null as no override. Treating null the same as a missing key here (($requestOverrides[SignatureFlowPolicy::KEY] ?? null) === null) would keep both paths consistent.

return;
}

throw new LibresignException(
// TRANSLATORS Error shown when someone tries to change whether signers sign in order after the signing flow already started.
$this->l10n?->t('The signing order cannot be changed after the signing flow has started.')
?? 'The signing order cannot be changed after the signing flow has started.',
Http::STATUS_UNPROCESSABLE_ENTITY,
);
}
}
10 changes: 9 additions & 1 deletion lib/Service/RequestSignatureService.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
use OCA\Libresign\Service\Validation\FileInputValidator;
use OCA\Libresign\Service\Validation\SignerValidator;
use OCA\Libresign\Service\Validation\SigningRequestValidator;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\Node;
Expand Down Expand Up @@ -371,9 +373,15 @@ public function saveFile(array $data): FileEntity {
if (!is_null($nodeId)) {
try {
$file = $this->fileMapper->getByNodeId($nodeId);
} catch (DoesNotExistException) {
Comment thread
paulo-raoni marked this conversation as resolved.
$file = null;
} catch (MultipleObjectsReturnedException) {
// TRANSLATORS Error shown when a document cannot be updated because LibreSign has more than one record for it.
throw new LibresignException($this->l10n->t('This document is registered more than once, so it cannot be updated. Please contact your administrator.'), 422);
}
if ($file !== null) {
$this->filePolicyApplier->syncAllPolicies($file, $data);
return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
} catch (\Throwable) {
}
}

Expand Down
8 changes: 8 additions & 0 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@
"size",
"docmdpLevel",
"signatureFlow",
"policySnapshotFrozen",
"visibleElements",
"signers",
"signersCount",
Expand Down Expand Up @@ -876,6 +877,9 @@
"ordered_numeric"
]
},
"policySnapshotFrozen": {
"type": "boolean"
},
"visibleElements": {
"type": "array",
"items": {
Expand Down Expand Up @@ -1559,6 +1563,7 @@
"metadata",
"docmdpLevel",
"signatureFlow",
"policySnapshotFrozen",
"signersCount",
"signers",
"requested_by",
Expand Down Expand Up @@ -1613,6 +1618,9 @@
"ordered_numeric"
]
},
"policySnapshotFrozen": {
"type": "boolean"
},
"signersCount": {
"type": "integer",
"format": "int64"
Expand Down
8 changes: 8 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@
"size",
"docmdpLevel",
"signatureFlow",
"policySnapshotFrozen",
"visibleElements",
"signers",
"signersCount",
Expand Down Expand Up @@ -577,6 +578,9 @@
"ordered_numeric"
]
},
"policySnapshotFrozen": {
"type": "boolean"
},
"visibleElements": {
"type": "array",
"items": {
Expand Down Expand Up @@ -1120,6 +1124,7 @@
"metadata",
"docmdpLevel",
"signatureFlow",
"policySnapshotFrozen",
"signersCount",
"signers",
"requested_by",
Expand Down Expand Up @@ -1174,6 +1179,9 @@
"ordered_numeric"
]
},
"policySnapshotFrozen": {
"type": "boolean"
},
"signersCount": {
"type": "integer",
"format": "int64"
Expand Down
Loading
Loading