Skip to content

Commit 7440034

Browse files
authored
Merge pull request #6021 from LibreSign/feat/docmdp-implementation
feat: docmdp implementation
2 parents eb7183c + 9088d4f commit 7440034

27 files changed

+2060
-573
lines changed

REUSE.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ path = [
5050
"src/types/openapi/openapi.ts",
5151
"tests/php/Unit/Handler/mock/cert.json",
5252
"tests/php/fixtures/cfssl/newcert-with-success.json",
53+
"tests/php/fixtures/real_jsignpdf_level1.pdf",
5354
"tests/php/fixtures/small_valid-signed.pdf",
5455
"tests/php/fixtures/small_valid.pdf",
5556
"tests/integration/composer.json",

lib/Controller/AdminController.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010

1111
use DateTimeInterface;
1212
use OCA\Libresign\AppInfo\Application;
13+
use OCA\Libresign\Enum\DocMdpLevel;
1314
use OCA\Libresign\Exception\LibresignException;
1415
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
1516
use OCA\Libresign\Handler\CertificateEngine\IEngineHandler;
1617
use OCA\Libresign\Helper\ConfigureCheckHelper;
1718
use OCA\Libresign\ResponseDefinitions;
1819
use OCA\Libresign\Service\Certificate\ValidateService;
1920
use OCA\Libresign\Service\CertificatePolicyService;
21+
use OCA\Libresign\Service\DocMdpConfigService;
2022
use OCA\Libresign\Service\FooterService;
2123
use OCA\Libresign\Service\Install\ConfigureCheckService;
2224
use OCA\Libresign\Service\Install\InstallService;
@@ -64,6 +66,7 @@ public function __construct(
6466
private ValidateService $validateService,
6567
private ReminderService $reminderService,
6668
private FooterService $footerService,
69+
private DocMdpConfigService $docMdpConfigService,
6770
) {
6871
parent::__construct(Application::APP_ID, $request);
6972
$this->eventSource = $this->eventSourceFactory->create();
@@ -857,4 +860,41 @@ public function footerTemplatePreviewPdf(string $template = '', int $width = 595
857860
], Http::STATUS_BAD_REQUEST);
858861
}
859862
}
863+
864+
/**
865+
* Set DocMDP configuration
866+
*
867+
* @param bool $enabled Enable or disable DocMDP certification
868+
* @param int $defaultLevel Default DocMDP level (0-3): 0=none, 1=no changes, 2=form fill, 3=form fill + annotations
869+
* @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{}>
870+
*
871+
* 200: Configuration saved successfully
872+
* 400: Invalid DocMDP level provided
873+
* 500: Internal server error
874+
*/
875+
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/docmdp/config', requirements: ['apiVersion' => '(v1)'])]
876+
public function setDocMdpConfig(bool $enabled, int $defaultLevel): DataResponse {
877+
try {
878+
$this->docMdpConfigService->setEnabled($enabled);
879+
880+
if ($enabled) {
881+
$level = DocMdpLevel::tryFrom($defaultLevel);
882+
if ($level === null) {
883+
return new DataResponse([
884+
'error' => $this->l10n->t('Invalid DocMDP level'),
885+
], Http::STATUS_BAD_REQUEST);
886+
}
887+
888+
$this->docMdpConfigService->setLevel($level);
889+
}
890+
891+
return new DataResponse([
892+
'message' => $this->l10n->t('Settings saved'),
893+
]);
894+
} catch (\Exception $e) {
895+
return new DataResponse([
896+
'error' => $e->getMessage(),
897+
], Http::STATUS_INTERNAL_SERVER_ERROR);
898+
}
899+
}
860900
}

lib/Db/SignRequest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
* @method string getDisplayName()
3131
* @method void setMetadata(array $metadata)
3232
* @method ?array getMetadata()
33+
* @method void setDocmdpLevel(int $docmdpLevel)
34+
* @method int getDocmdpLevel()
3335
*/
3436
class SignRequest extends Entity {
3537
protected ?int $fileId = null;
@@ -40,6 +42,7 @@ class SignRequest extends Entity {
4042
protected ?\DateTime $signed = null;
4143
protected ?string $signedHash = null;
4244
protected ?array $metadata = null;
45+
protected int $docmdpLevel = 0;
4346
public function __construct() {
4447
$this->addType('id', Types::INTEGER);
4548
$this->addType('fileId', Types::INTEGER);
@@ -50,5 +53,6 @@ public function __construct() {
5053
$this->addType('signed', Types::DATETIME);
5154
$this->addType('signedHash', Types::STRING);
5255
$this->addType('metadata', Types::JSON);
56+
$this->addType('docmdpLevel', Types::SMALLINT);
5357
}
5458
}

lib/Enum/DocMdpLevel.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@ public function getLabel(IL10N $l10n): string {
2525
return match($this) {
2626
self::NOT_CERTIFIED => $l10n->t('No certification'),
2727
self::CERTIFIED_NO_CHANGES_ALLOWED => $l10n->t('No changes allowed'),
28-
self::CERTIFIED_FORM_FILLING => $l10n->t('Form filling and additional signatures'),
29-
self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('Form filling, annotations and additional signatures'),
28+
self::CERTIFIED_FORM_FILLING => $l10n->t('Form filling allowed'),
29+
self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('Form filling and commenting allowed'),
3030
};
3131
}
3232

3333
public function getDescription(IL10N $l10n): string {
3434
return match($this) {
35-
self::NOT_CERTIFIED => $l10n->t('Document is not certified. No restrictions on modifications.'),
36-
self::CERTIFIED_NO_CHANGES_ALLOWED => $l10n->t('No changes allowed. Additional approval signatures are prohibited.'),
37-
self::CERTIFIED_FORM_FILLING => $l10n->t('Form filling allowed. Additional approval signatures are allowed.'),
38-
self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('Form filling and annotations allowed. Additional approval signatures are allowed.'),
35+
self::NOT_CERTIFIED => $l10n->t('The document is not certified; edits and new signatures are allowed, but any change will mark previous signatures as modified.'),
36+
self::CERTIFIED_NO_CHANGES_ALLOWED => $l10n->t('After the first signature, no further edits or signatures are allowed; any change invalidates the certification.'),
37+
self::CERTIFIED_FORM_FILLING => $l10n->t('After the first signature, only form filling and additional signatures are allowed; other changes invalidate the certification.'),
38+
self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('After the first signature, form filling, comments, and additional signatures are allowed; other changes invalidate the certification.'),
3939
};
4040
}
4141
}

lib/Handler/DocMdpHandler.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public function __construct(
2626
) {
2727
}
2828

29+
public function allowsAdditionalSignatures($resource): bool {
30+
$docmdpLevel = $this->extractDocMdpLevel($resource);
31+
32+
return $docmdpLevel !== DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED;
33+
}
34+
2935
public function extractDocMdpData($resource): array {
3036
if (!is_resource($resource)) {
3137
return [];
@@ -160,15 +166,15 @@ private function extractPValueFromIndirectReference(string $content, string $ind
160166
* @return array Array of objects with keys: objNum, dict, position
161167
*/
162168
private function parsePdfObjects(string $content): array {
163-
if (!preg_match_all('/(\d+)\s+\d+\s+obj\s*(<<.*?>>)\s*endobj/s', $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
169+
if (!preg_match_all('/(\d+)\s+\d+\s+obj(.*?)endobj/s', $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
164170
return [];
165171
}
166172

167173
$objects = [];
168174
foreach ($matches as $match) {
169175
$objects[] = [
170176
'objNum' => $match[1][0],
171-
'dict' => $match[2][0],
177+
'dict' => trim($match[2][0]),
172178
'position' => $match[2][1],
173179
];
174180
}
@@ -452,16 +458,11 @@ private function validateSignatureDictionary(string $content): bool {
452458
return $this->validateDictionaryEntries($sigDict);
453459
}
454460

455-
/**
456-
* Find signature dictionary with /Reference entry
457-
*
458-
* @param array $objects Parsed PDF objects
459-
* @return string|null Dictionary content or null
460-
*/
461461
private function findSignatureDictionary(array $objects): ?string {
462462
foreach ($objects as $obj) {
463-
if (preg_match('/\/Reference\s*\[/', $obj['dict'])) {
464-
return $obj['dict'];
463+
$dict = $obj['dict'];
464+
if (preg_match('/\/Type\s*\/Sig\b/', $dict) && preg_match('/\/Reference\s*\[/', $dict)) {
465+
return $dict;
465466
}
466467
}
467468
return null;
@@ -474,7 +475,7 @@ private function findSignatureDictionary(array $objects): ?string {
474475
* @return bool True if all required entries are valid
475476
*/
476477
private function validateDictionaryEntries(string $dict): bool {
477-
if (preg_match('/\/Type\s*\/(\w+)/', $dict, $typeMatch) && $typeMatch[1] !== 'Sig') {
478+
if (!preg_match('/\/Type\s*\/Sig\b/', $dict)) {
478479
return false;
479480
}
480481

lib/Handler/SignEngine/JSignPdfHandler.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OCA\Libresign\Exception\LibresignException;
1515
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
1616
use OCA\Libresign\Helper\JavaHelper;
17+
use OCA\Libresign\Service\DocMdpConfigService;
1718
use OCA\Libresign\Service\Install\InstallService;
1819
use OCA\Libresign\Service\SignatureBackgroundService;
1920
use OCA\Libresign\Service\SignatureTextService;
@@ -40,6 +41,7 @@ public function __construct(
4041
private SignatureBackgroundService $signatureBackgroundService,
4142
protected CertificateEngineFactory $certificateEngineFactory,
4243
protected JavaHelper $javaHelper,
44+
private DocMdpConfigService $docMdpConfigService,
4345
) {
4446
}
4547

@@ -86,6 +88,11 @@ public function getJSignParam(): JSignParam {
8688
. ' -Duser.home=' . escapeshellarg($this->getHome()) . ' '
8789
);
8890
}
91+
92+
$certificationLevel = $this->getCertificationLevel();
93+
if ($certificationLevel !== null) {
94+
$this->jSignParam->setJSignParameters(' -cl ' . $certificationLevel);
95+
}
8996
}
9097
return $this->jSignParam;
9198
}
@@ -147,6 +154,14 @@ private function getHashAlgorithm(): string {
147154
return 'SHA256';
148155
}
149156

157+
private function getCertificationLevel(): ?string {
158+
if (!$this->docMdpConfigService->isEnabled()) {
159+
return null;
160+
}
161+
162+
return $this->docMdpConfigService->getLevel()->name;
163+
}
164+
150165
#[\Override]
151166
public function sign(): File {
152167
$this->beforeSign();

lib/Migration/Version14000Date20251206120000.php

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,29 @@ class Version14000Date20251206120000 extends SimpleMigrationStep {
2525
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
2626
/** @var ISchemaWrapper $schema */
2727
$schema = $schemaClosure();
28-
$table = $schema->getTable('libresign_file');
2928

30-
if (!$table->hasColumn('modification_status')) {
31-
$table->addColumn('modification_status', Types::SMALLINT, [
32-
'notnull' => true,
33-
'default' => 0,
34-
'comment' => 'DocMDP modification detection status: 0=unchecked, 1=unmodified, 2=allowed, 3=violation',
35-
]);
36-
return $schema;
29+
if ($schema->hasTable('libresign_sign_request')) {
30+
$tableSignRequest = $schema->getTable('libresign_sign_request');
31+
if (!$tableSignRequest->hasColumn('docmdp_level')) {
32+
$tableSignRequest->addColumn('docmdp_level', Types::SMALLINT, [
33+
'notnull' => true,
34+
'default' => 0,
35+
'comment' => 'DocMDP permission level: 0=none, 1=no changes, 2=form fill, 3=form fill + annotations',
36+
]);
37+
}
3738
}
3839

39-
return null;
40+
if ($schema->hasTable('libresign_file')) {
41+
$tableFile = $schema->getTable('libresign_file');
42+
if (!$tableFile->hasColumn('modification_status')) {
43+
$tableFile->addColumn('modification_status', Types::SMALLINT, [
44+
'notnull' => true,
45+
'default' => 0,
46+
'comment' => 'DocMDP modification detection status: 0=unchecked, 1=unmodified, 2=allowed, 3=violation',
47+
]);
48+
}
49+
}
50+
51+
return $schema;
4052
}
4153
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Libresign\Service;
11+
12+
use OCA\Libresign\AppInfo\Application;
13+
use OCA\Libresign\Enum\DocMdpLevel;
14+
use OCP\IAppConfig;
15+
use OCP\IL10N;
16+
17+
class DocMdpConfigService {
18+
private const CONFIG_KEY_LEVEL = 'docmdp_level';
19+
20+
public function __construct(
21+
private IAppConfig $appConfig,
22+
private IL10N $l10n,
23+
) {
24+
}
25+
26+
public function isEnabled(): bool {
27+
return $this->appConfig->hasKey(Application::APP_ID, self::CONFIG_KEY_LEVEL);
28+
}
29+
30+
public function setEnabled(bool $enabled): void {
31+
if (!$enabled) {
32+
$this->appConfig->deleteKey(Application::APP_ID, self::CONFIG_KEY_LEVEL);
33+
}
34+
}
35+
36+
public function getLevel(): DocMdpLevel {
37+
$level = $this->appConfig->getValueInt(Application::APP_ID, self::CONFIG_KEY_LEVEL, DocMdpLevel::NOT_CERTIFIED->value);
38+
return DocMdpLevel::from($level);
39+
}
40+
41+
public function setLevel(DocMdpLevel $level): void {
42+
$this->appConfig->setValueInt(Application::APP_ID, self::CONFIG_KEY_LEVEL, $level->value);
43+
}
44+
45+
public function getConfig(): array {
46+
return [
47+
'enabled' => $this->isEnabled(),
48+
'defaultLevel' => $this->getLevel()->value,
49+
'availableLevels' => $this->getAvailableLevels(),
50+
];
51+
}
52+
53+
private function getAvailableLevels(): array {
54+
return array_map(
55+
fn (DocMdpLevel $level) => [
56+
'value' => $level->value,
57+
'label' => $level->getLabel($this->l10n),
58+
'description' => $level->getDescription($this->l10n),
59+
],
60+
DocMdpLevel::cases()
61+
);
62+
}
63+
}

0 commit comments

Comments
 (0)