Skip to content

Commit cbd5751

Browse files
authored
Merge pull request #6071 from LibreSign/backport/6069/stable32
[stable32] feat: sequential signing
2 parents a5b0973 + 8e2b22a commit cbd5751

33 files changed

Lines changed: 1723 additions & 22 deletions

lib/Controller/AdminController.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,47 @@ public function footerTemplatePreviewPdf(string $template = '', int $width = 595
895895
}
896896
}
897897

898+
/**
899+
* Set signature flow configuration
900+
*
901+
* @param string $mode Signature flow mode: 'parallel' or 'ordered_numeric'
902+
* @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{error: string}, array{}>
903+
*
904+
* 200: Configuration saved successfully
905+
* 400: Invalid signature flow mode provided
906+
* 500: Internal server error
907+
*/
908+
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signature-flow/config', requirements: ['apiVersion' => '(v1)'])]
909+
public function setSignatureFlowConfig(string $mode): DataResponse {
910+
try {
911+
$signatureFlow = \OCA\Libresign\Service\SignatureFlow::from($mode);
912+
} catch (\ValueError) {
913+
return new DataResponse([
914+
'error' => $this->l10n->t('Invalid signature flow mode. Use "parallel" or "ordered_numeric".'),
915+
], Http::STATUS_BAD_REQUEST);
916+
}
917+
918+
try {
919+
if ($signatureFlow === \OCA\Libresign\Service\SignatureFlow::PARALLEL) {
920+
$this->appConfig->deleteKey(Application::APP_ID, 'signature_flow');
921+
} else {
922+
$this->appConfig->setValueString(
923+
Application::APP_ID,
924+
'signature_flow',
925+
$signatureFlow->value
926+
);
927+
}
928+
929+
return new DataResponse([
930+
'message' => $this->l10n->t('Settings saved'),
931+
]);
932+
} catch (\Exception $e) {
933+
return new DataResponse([
934+
'error' => $e->getMessage(),
935+
], Http::STATUS_INTERNAL_SERVER_ERROR);
936+
}
937+
}
938+
898939
/**
899940
* Set DocMDP configuration
900941
*

lib/Controller/PageController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public function index(): TemplateResponse {
9595

9696
$this->provideSignerSignatues();
9797
$this->initialState->provideInitialState('identify_methods', $this->identifyMethodService->getIdentifyMethodsSettings());
98+
$this->initialState->provideInitialState('signature_flow', $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', \OCA\Libresign\Service\SignatureFlow::PARALLEL->value));
9899
$this->initialState->provideInitialState('legal_information', $this->appConfig->getValueString(Application::APP_ID, 'legal_information'));
99100

100101
Util::addScript(Application::APP_ID, 'libresign-main');

lib/Controller/RequestSignatureController.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ public function __construct(
4747
/**
4848
* Request signature
4949
*
50-
* Request that a file be signed by a group of people
50+
* Request that a file be signed by a group of people.
51+
* Each user in the users array can optionally include a 'signing_order' field
52+
* to control the order of signatures when ordered signing flow is enabled.
5153
*
5254
* @param LibresignNewFile $file File object.
53-
* @param LibresignNewSigner[] $users Collection of users who must sign the document
55+
* @param LibresignNewSigner[] $users Collection of users who must sign the document. Each user can have: identify, displayName, description, notify, signing_order
5456
* @param string $name The name of file to sign
5557
* @param string|null $callback URL that will receive a POST after the document is signed
5658
* @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending

lib/Db/SignRequest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
* @method ?array getMetadata()
3333
* @method void setDocmdpLevel(int $docmdpLevel)
3434
* @method int getDocmdpLevel()
35+
* @method void setSigningOrder(int $signingOrder)
36+
* @method int getSigningOrder()
37+
* @method void setStatus(int $status)
38+
* @method int getStatus()
3539
*/
3640
class SignRequest extends Entity {
3741
protected ?int $fileId = null;
@@ -43,6 +47,9 @@ class SignRequest extends Entity {
4347
protected ?string $signedHash = null;
4448
protected ?array $metadata = null;
4549
protected int $docmdpLevel = 0;
50+
protected int $signingOrder = 1;
51+
protected int $status = 0;
52+
4653
public function __construct() {
4754
$this->addType('id', Types::INTEGER);
4855
$this->addType('fileId', Types::INTEGER);
@@ -54,5 +61,15 @@ public function __construct() {
5461
$this->addType('signedHash', Types::STRING);
5562
$this->addType('metadata', Types::JSON);
5663
$this->addType('docmdpLevel', Types::SMALLINT);
64+
$this->addType('signingOrder', Types::INTEGER);
65+
$this->addType('status', Types::SMALLINT);
66+
}
67+
68+
public function getStatusEnum(): SignRequestStatus {
69+
return SignRequestStatus::from($this->status);
70+
}
71+
72+
public function setStatusEnum(SignRequestStatus $status): void {
73+
$this->setStatus($status->value);
5774
}
5875
}

lib/Db/SignRequestStatus.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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\Db;
10+
11+
enum SignRequestStatus: int {
12+
case DRAFT = 0;
13+
case ABLE_TO_SIGN = 1;
14+
case SIGNED = 2;
15+
}

lib/Files/TemplateLoader.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace OCA\Libresign\Files;
1010

1111
use OCA\Files\Event\LoadSidebar;
12+
use OCA\Libresign\AppInfo\Application;
1213
use OCA\Libresign\Exception\LibresignException;
1314
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
1415
use OCA\Libresign\Helper\ValidateHelper;
@@ -18,6 +19,7 @@
1819
use OCP\EventDispatcher\Event;
1920
use OCP\EventDispatcher\IEventDispatcher;
2021
use OCP\EventDispatcher\IEventListener;
22+
use OCP\IAppConfig;
2123
use OCP\IRequest;
2224
use OCP\IUserSession;
2325

@@ -33,6 +35,7 @@ public function __construct(
3335
private ValidateHelper $validateHelper,
3436
private IdentifyMethodService $identifyMethodService,
3537
private CertificateEngineFactory $certificateEngineFactory,
38+
private IAppConfig $appConfig,
3639
) {
3740
}
3841

@@ -54,6 +57,11 @@ public function handle(Event $event): void {
5457
$this->identifyMethodService->getIdentifyMethodsSettings()
5558
);
5659

60+
$this->initialState->provideInitialState(
61+
'signature_flow',
62+
$this->appConfig->getValueString(Application::APP_ID, 'signature_flow', \OCA\Libresign\Service\SignatureFlow::PARALLEL->value)
63+
);
64+
5765
try {
5866
$this->validateHelper->canRequestSign($this->userSession->getUser());
5967
$this->initialState->provideInitialState('can_request_sign', true);

lib/Helper/ValidateHelper.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,9 +709,32 @@ public function validateFileUuid(array $data): void {
709709

710710
public function validateSigner(string $uuid, ?IUser $user = null): void {
711711
$this->validateSignerUuidExists($uuid);
712+
$this->validateSignerStatus($uuid);
712713
$this->validateIdentifyMethod($uuid, $user);
713714
}
714715

716+
/**
717+
* @throws LibresignException
718+
*/
719+
private function validateSignerStatus(string $uuid): void {
720+
$signRequest = $this->signRequestMapper->getByUuid($uuid);
721+
$status = $signRequest->getStatusEnum();
722+
723+
if ($status === \OCA\Libresign\Db\SignRequestStatus::DRAFT) {
724+
throw new LibresignException(json_encode([
725+
'action' => JSActions::ACTION_DO_NOTHING,
726+
'errors' => [['message' => $this->l10n->t('You are not allowed to sign this document yet')]],
727+
]));
728+
}
729+
730+
if ($status === \OCA\Libresign\Db\SignRequestStatus::SIGNED) {
731+
throw new LibresignException(json_encode([
732+
'action' => JSActions::ACTION_DO_NOTHING,
733+
'errors' => [['message' => $this->l10n->t('Document already signed')]],
734+
]));
735+
}
736+
}
737+
715738
public function validateRenewSigner(string $uuid, ?IUser $user = null): void {
716739
$this->validateSignerUuidExists($uuid);
717740
$signRequest = $this->signRequestMapper->getByUuid($uuid);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\Migration;
10+
11+
use Closure;
12+
use OCP\DB\ISchemaWrapper;
13+
use OCP\DB\Types;
14+
use OCP\Migration\IOutput;
15+
use OCP\Migration\SimpleMigrationStep;
16+
17+
/**
18+
* Add sequential signing support
19+
* - Adds 'signing_order', 'status', and 'released_at' columns to libresign_sign_request table
20+
*/
21+
class Version15000Date20251209000000 extends SimpleMigrationStep {
22+
/**
23+
* @param IOutput $output
24+
* @param Closure(): ISchemaWrapper $schemaClosure
25+
* @param array $options
26+
* @return null|ISchemaWrapper
27+
*/
28+
#[\Override]
29+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
30+
/** @var ISchemaWrapper $schema */
31+
$schema = $schemaClosure();
32+
33+
// Add signing order, status, and released_at to SignRequest table
34+
if ($schema->hasTable('libresign_sign_request')) {
35+
$tableSignRequest = $schema->getTable('libresign_sign_request');
36+
if (!$tableSignRequest->hasColumn('signing_order')) {
37+
$tableSignRequest->addColumn('signing_order', Types::INTEGER, [
38+
'notnull' => true,
39+
'default' => 1,
40+
'comment' => 'Numeric order/stage for sequential signing (e.g., 1, 2, 3)',
41+
]);
42+
}
43+
if (!$tableSignRequest->hasColumn('status')) {
44+
$tableSignRequest->addColumn('status', Types::SMALLINT, [
45+
'notnull' => true,
46+
'default' => 0,
47+
'comment' => 'Status: 0=draft, 1=able_to_sign, 2=signed',
48+
]);
49+
}
50+
}
51+
52+
return $schema;
53+
}
54+
}

lib/ResponseDefinitions.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* email?: string,
2929
* account?: string,
3030
* },
31+
* signingOrder?: non-negative-int,
3132
* }
3233
* @psalm-type LibresignNewFile = array{
3334
* base64?: string,
@@ -172,6 +173,7 @@
172173
* hash_algorithm?: string,
173174
* me: bool,
174175
* signRequestId: non-negative-int,
176+
* signingOrder?: non-negative-int,
175177
* identifyMethods?: LibresignIdentifyMethod[],
176178
* visibleElements?: LibresignVisibleElement[],
177179
* signatureMethods?: LibresignSignatureMethods,

lib/Service/FileService.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ private function loadLibreSignSigners(): void {
387387
$this->fileData->signers[$index]['me'] = false;
388388
$this->fileData->signers[$index]['signRequestId'] = $signer->getId();
389389
$this->fileData->signers[$index]['description'] = $signer->getDescription();
390+
$this->fileData->signers[$index]['signingOrder'] = $signer->getSigningOrder();
390391
$this->fileData->signers[$index]['visibleElements'] = $this->getVisibleElements($signer->getId());
391392
$this->fileData->signers[$index]['request_sign_date'] = $signer->getCreatedAt()->format(DateTimeInterface::ATOM);
392393
if (empty($this->fileData->signers[$index]['signed'])) {
@@ -468,6 +469,18 @@ private function loadLibreSignSigners(): void {
468469
}, []);
469470
ksort($this->fileData->signers[$index]);
470471
}
472+
473+
usort($this->fileData->signers, function ($a, $b) {
474+
$orderA = $a['signingOrder'] ?? PHP_INT_MAX;
475+
$orderB = $b['signingOrder'] ?? PHP_INT_MAX;
476+
477+
if ($orderA !== $orderB) {
478+
return $orderA <=> $orderB;
479+
}
480+
481+
return ($a['signRequestId'] ?? 0) <=> ($b['signRequestId'] ?? 0);
482+
});
483+
471484
$this->signersLibreSignLoaded = true;
472485
}
473486

@@ -821,6 +834,7 @@ private function associateAllAndFormat(IUser $user, array $files, array $signers
821834
'request_sign_date' => $signer->getCreatedAt()->format(DateTimeInterface::ATOM),
822835
'signed' => null,
823836
'signRequestId' => $signer->getId(),
837+
'signingOrder' => $signer->getSigningOrder(),
824838
'me' => array_reduce($identifyMethodsOfSigner, function (bool $carry, IdentifyMethod $identifyMethod) use ($user): bool {
825839
if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT) {
826840
if ($user->getUID() === $identifyMethod->getIdentifierValue()) {
@@ -888,6 +902,17 @@ private function associateAllAndFormat(IUser $user, array $files, array $signers
888902
$files[$key]['signers'] = [];
889903
$files[$key]['statusText'] = $this->l10n->t('no signers');
890904
} else {
905+
usort($files[$key]['signers'], function ($a, $b) {
906+
$orderA = $a['signingOrder'] ?? PHP_INT_MAX;
907+
$orderB = $b['signingOrder'] ?? PHP_INT_MAX;
908+
909+
if ($orderA !== $orderB) {
910+
return $orderA <=> $orderB;
911+
}
912+
913+
return ($a['signRequestId'] ?? 0) <=> ($b['signRequestId'] ?? 0);
914+
});
915+
891916
$files[$key]['statusText'] = $this->fileMapper->getTextOfStatus((int)$files[$key]['status']);
892917
}
893918
unset($files[$key]['id']);

0 commit comments

Comments
 (0)