Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
bced036
feat: add DocMDP database schema
vitormattos Dec 6, 2025
d433701
feat: add docmdpLevel property to SignRequest entity
vitormattos Dec 6, 2025
3b417e7
feat: add DocMdpConfigService for admin configuration
vitormattos Dec 6, 2025
89a5eb9
feat: integrate DocMDP certification level in JSignPdfHandler
vitormattos Dec 6, 2025
8c44af4
feat: add DocMDP configuration API endpoint
vitormattos Dec 6, 2025
577aea7
feat: provide DocMDP config to admin settings initial state
vitormattos Dec 6, 2025
074cb31
feat: add DocMDP admin settings UI component
vitormattos Dec 6, 2025
e6bd436
feat: expose DocMDP data in file validation response
vitormattos Dec 6, 2025
0b0ed79
docs: fix OpenAPI documentation for setDocMdpConfig endpoint
vitormattos Dec 6, 2025
a66384c
chore: update OpenAPI specs for DocMDP endpoint
vitormattos Dec 6, 2025
6b6634c
style: fix code style (phpcs)
vitormattos Dec 6, 2025
b997a5f
test: fix AdminTest and FileServiceTest for DocMDP implementation
vitormattos Dec 6, 2025
d875a35
refactor: remove duplicate methods and improve DocMDP descriptions
vitormattos Dec 7, 2025
23eceae
test: add PdfFixtureTrait for shared PDF test fixtures
vitormattos Dec 7, 2025
9d65c3b
feat: add DocMdpHandler::allowsAdditionalSignatures()
vitormattos Dec 7, 2025
d14047d
refactor: extract validateFileContent() to TFile trait
vitormattos Dec 7, 2025
9b609da
feat: inject DocMdpHandler into FileService
vitormattos Dec 7, 2025
01ff9e7
test: fix FileServiceTest constructor and validation assertions
vitormattos Dec 7, 2025
807125d
test: consolidate DocMdpHandlerTest PDF fixtures
vitormattos Dec 7, 2025
743f5b6
feat: inject DocMdpHandler into RequestSignatureService
vitormattos Dec 7, 2025
130ab29
feat: add DocMDP settings UI and validation display
vitormattos Dec 7, 2025
33d8ca6
style: fix phpcs import order in DocMdpHandlerTest
vitormattos Dec 7, 2025
85940ae
test: improve testValidateDocMdpAllowsSignatures test structure
vitormattos Dec 8, 2025
5bbc90b
refactor: extract getLibreSignFileAsResource method to reduce coupling
vitormattos Dec 8, 2025
0dd43eb
feat: add DocMDP validation handler for PDF signature permissions
vitormattos Dec 8, 2025
154a142
test: add comprehensive tests for DocMdpHandler
vitormattos Dec 8, 2025
a15b009
fix: cs
vitormattos Dec 8, 2025
9359d72
fix: add error handling for fopen failure in getLibreSignFileAsResource
vitormattos Dec 8, 2025
f780ef0
chore: add pending fixture file
vitormattos Dec 8, 2025
c7335b5
fix: add missing DocMdpHandler mock in RequestSignatureServiceTest
vitormattos Dec 8, 2025
d3c0594
chore: add real_jsignpdf_level1.pdf to REUSE.toml
vitormattos Dec 8, 2025
07e9921
Update error message: use more generic phrase for PDF resource creati…
vitormattos Dec 8, 2025
977f110
Use php://memory instead of php://temp for PDF resource creation (in-…
vitormattos Dec 8, 2025
5fc98d4
Use php://memory instead of php://temp in SignFileServiceTest for in-…
vitormattos Dec 8, 2025
7947b23
chore: use a most generic text
vitormattos Dec 8, 2025
7ffee0d
Update DocMdpLevel labels and descriptions for clarity and user-frien…
vitormattos Dec 8, 2025
4413d3b
fix: update logic in SignFileService.php\n\nChanged implementation at…
vitormattos Dec 8, 2025
6812273
fix: update logic in TFile.php
vitormattos Dec 8, 2025
ac6d795
fix: use generic error messages for DocMDP configuration\n\nChanged e…
vitormattos Dec 8, 2025
7aa85ce
chore: code style fixes in DocMdpLevel.php
vitormattos Dec 8, 2025
0a578c2
test: remove exception message validation from DocMDP tests
vitormattos Dec 8, 2025
5599183
docs: add TRANSLATORS comment explaining DocMDP for translators
vitormattos Dec 8, 2025
0668e31
test: update DocMDP level 1 exception test to only check exception type
vitormattos Dec 8, 2025
9088d4f
test: refactor DocMDP level 1 exception test to use getService helper
vitormattos Dec 8, 2025
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
1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ path = [
"src/types/openapi/openapi.ts",
"tests/php/Unit/Handler/mock/cert.json",
"tests/php/fixtures/cfssl/newcert-with-success.json",
"tests/php/fixtures/real_jsignpdf_level1.pdf",
"tests/php/fixtures/small_valid-signed.pdf",
"tests/php/fixtures/small_valid.pdf",
"tests/integration/composer.json",
Expand Down
40 changes: 40 additions & 0 deletions lib/Controller/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@

use DateTimeInterface;
use OCA\Libresign\AppInfo\Application;
use OCA\Libresign\Enum\DocMdpLevel;
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
use OCA\Libresign\Handler\CertificateEngine\IEngineHandler;
use OCA\Libresign\Helper\ConfigureCheckHelper;
use OCA\Libresign\ResponseDefinitions;
use OCA\Libresign\Service\Certificate\ValidateService;
use OCA\Libresign\Service\CertificatePolicyService;
use OCA\Libresign\Service\DocMdpConfigService;
use OCA\Libresign\Service\FooterService;
use OCA\Libresign\Service\Install\ConfigureCheckService;
use OCA\Libresign\Service\Install\InstallService;
Expand Down Expand Up @@ -64,6 +66,7 @@ public function __construct(
private ValidateService $validateService,
private ReminderService $reminderService,
private FooterService $footerService,
private DocMdpConfigService $docMdpConfigService,
) {
parent::__construct(Application::APP_ID, $request);
$this->eventSource = $this->eventSourceFactory->create();
Expand Down Expand Up @@ -857,4 +860,41 @@ public function footerTemplatePreviewPdf(string $template = '', int $width = 595
], Http::STATUS_BAD_REQUEST);
}
}

/**
* Set DocMDP configuration
*
* @param bool $enabled Enable or disable DocMDP certification
* @param int $defaultLevel Default DocMDP level (0-3): 0=none, 1=no changes, 2=form fill, 3=form fill + annotations
* @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{}>
*
* 200: Configuration saved successfully
* 400: Invalid DocMDP level provided
* 500: Internal server error
*/
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/docmdp/config', requirements: ['apiVersion' => '(v1)'])]
public function setDocMdpConfig(bool $enabled, int $defaultLevel): DataResponse {
try {
$this->docMdpConfigService->setEnabled($enabled);

if ($enabled) {
$level = DocMdpLevel::tryFrom($defaultLevel);
if ($level === null) {
return new DataResponse([
'error' => $this->l10n->t('Invalid DocMDP level'),
], Http::STATUS_BAD_REQUEST);
}

$this->docMdpConfigService->setLevel($level);
}

return new DataResponse([
'message' => $this->l10n->t('Settings saved'),
]);
} catch (\Exception $e) {
return new DataResponse([
'error' => $e->getMessage(),
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
}
4 changes: 4 additions & 0 deletions lib/Db/SignRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
* @method string getDisplayName()
* @method void setMetadata(array $metadata)
* @method ?array getMetadata()
* @method void setDocmdpLevel(int $docmdpLevel)
* @method int getDocmdpLevel()
*/
class SignRequest extends Entity {
protected ?int $fileId = null;
Expand All @@ -40,6 +42,7 @@ class SignRequest extends Entity {
protected ?\DateTime $signed = null;
protected ?string $signedHash = null;
protected ?array $metadata = null;
protected int $docmdpLevel = 0;
public function __construct() {
$this->addType('id', Types::INTEGER);
$this->addType('fileId', Types::INTEGER);
Expand All @@ -50,5 +53,6 @@ public function __construct() {
$this->addType('signed', Types::DATETIME);
$this->addType('signedHash', Types::STRING);
$this->addType('metadata', Types::JSON);
$this->addType('docmdpLevel', Types::SMALLINT);
}
}
12 changes: 6 additions & 6 deletions lib/Enum/DocMdpLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ public function getLabel(IL10N $l10n): string {
return match($this) {
self::NOT_CERTIFIED => $l10n->t('No certification'),
self::CERTIFIED_NO_CHANGES_ALLOWED => $l10n->t('No changes allowed'),
self::CERTIFIED_FORM_FILLING => $l10n->t('Form filling and additional signatures'),
self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('Form filling, annotations and additional signatures'),
self::CERTIFIED_FORM_FILLING => $l10n->t('Form filling allowed'),
self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('Form filling and commenting allowed'),
};
}

public function getDescription(IL10N $l10n): string {
return match($this) {
self::NOT_CERTIFIED => $l10n->t('Document is not certified. No restrictions on modifications.'),
self::CERTIFIED_NO_CHANGES_ALLOWED => $l10n->t('No changes allowed. Additional approval signatures are prohibited.'),
self::CERTIFIED_FORM_FILLING => $l10n->t('Form filling allowed. Additional approval signatures are allowed.'),
self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('Form filling and annotations allowed. Additional approval signatures are allowed.'),
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.'),
self::CERTIFIED_NO_CHANGES_ALLOWED => $l10n->t('After the first signature, no further edits or signatures are allowed; any change invalidates the certification.'),
self::CERTIFIED_FORM_FILLING => $l10n->t('After the first signature, only form filling and additional signatures are allowed; other changes invalidate the certification.'),
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.'),
};
}
}
23 changes: 12 additions & 11 deletions lib/Handler/DocMdpHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ public function __construct(
) {
}

public function allowsAdditionalSignatures($resource): bool {
$docmdpLevel = $this->extractDocMdpLevel($resource);

return $docmdpLevel !== DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED;
}

public function extractDocMdpData($resource): array {
if (!is_resource($resource)) {
return [];
Expand Down Expand Up @@ -160,15 +166,15 @@ private function extractPValueFromIndirectReference(string $content, string $ind
* @return array Array of objects with keys: objNum, dict, position
*/
private function parsePdfObjects(string $content): array {
if (!preg_match_all('/(\d+)\s+\d+\s+obj\s*(<<.*?>>)\s*endobj/s', $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
if (!preg_match_all('/(\d+)\s+\d+\s+obj(.*?)endobj/s', $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
return [];
}

$objects = [];
foreach ($matches as $match) {
$objects[] = [
'objNum' => $match[1][0],
'dict' => $match[2][0],
'dict' => trim($match[2][0]),
'position' => $match[2][1],
];
}
Expand Down Expand Up @@ -452,16 +458,11 @@ private function validateSignatureDictionary(string $content): bool {
return $this->validateDictionaryEntries($sigDict);
}

/**
* Find signature dictionary with /Reference entry
*
* @param array $objects Parsed PDF objects
* @return string|null Dictionary content or null
*/
private function findSignatureDictionary(array $objects): ?string {
foreach ($objects as $obj) {
if (preg_match('/\/Reference\s*\[/', $obj['dict'])) {
return $obj['dict'];
$dict = $obj['dict'];
if (preg_match('/\/Type\s*\/Sig\b/', $dict) && preg_match('/\/Reference\s*\[/', $dict)) {
return $dict;
}
}
return null;
Expand All @@ -474,7 +475,7 @@ private function findSignatureDictionary(array $objects): ?string {
* @return bool True if all required entries are valid
*/
private function validateDictionaryEntries(string $dict): bool {
if (preg_match('/\/Type\s*\/(\w+)/', $dict, $typeMatch) && $typeMatch[1] !== 'Sig') {
if (!preg_match('/\/Type\s*\/Sig\b/', $dict)) {
return false;
}

Expand Down
15 changes: 15 additions & 0 deletions lib/Handler/SignEngine/JSignPdfHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
use OCA\Libresign\Helper\JavaHelper;
use OCA\Libresign\Service\DocMdpConfigService;
use OCA\Libresign\Service\Install\InstallService;
use OCA\Libresign\Service\SignatureBackgroundService;
use OCA\Libresign\Service\SignatureTextService;
Expand All @@ -40,6 +41,7 @@ public function __construct(
private SignatureBackgroundService $signatureBackgroundService,
protected CertificateEngineFactory $certificateEngineFactory,
protected JavaHelper $javaHelper,
private DocMdpConfigService $docMdpConfigService,
) {
}

Expand Down Expand Up @@ -86,6 +88,11 @@ public function getJSignParam(): JSignParam {
. ' -Duser.home=' . escapeshellarg($this->getHome()) . ' '
);
}

$certificationLevel = $this->getCertificationLevel();
if ($certificationLevel !== null) {
$this->jSignParam->setJSignParameters(' -cl ' . $certificationLevel);
}
}
return $this->jSignParam;
}
Expand Down Expand Up @@ -147,6 +154,14 @@ private function getHashAlgorithm(): string {
return 'SHA256';
}

private function getCertificationLevel(): ?string {
if (!$this->docMdpConfigService->isEnabled()) {
return null;
}

return $this->docMdpConfigService->getLevel()->name;
}

#[\Override]
public function sign(): File {
$this->beforeSign();
Expand Down
30 changes: 21 additions & 9 deletions lib/Migration/Version14000Date20251206120000.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,29 @@ class Version14000Date20251206120000 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('libresign_file');

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

return null;
if ($schema->hasTable('libresign_file')) {
$tableFile = $schema->getTable('libresign_file');
if (!$tableFile->hasColumn('modification_status')) {
$tableFile->addColumn('modification_status', Types::SMALLINT, [
'notnull' => true,
'default' => 0,
'comment' => 'DocMDP modification detection status: 0=unchecked, 1=unmodified, 2=allowed, 3=violation',
]);
}
}

return $schema;
}
}
63 changes: 63 additions & 0 deletions lib/Service/DocMdpConfigService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

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

namespace OCA\Libresign\Service;

use OCA\Libresign\AppInfo\Application;
use OCA\Libresign\Enum\DocMdpLevel;
use OCP\IAppConfig;
use OCP\IL10N;

class DocMdpConfigService {
private const CONFIG_KEY_LEVEL = 'docmdp_level';

public function __construct(
private IAppConfig $appConfig,
private IL10N $l10n,
) {
}

public function isEnabled(): bool {
return $this->appConfig->hasKey(Application::APP_ID, self::CONFIG_KEY_LEVEL);
}

public function setEnabled(bool $enabled): void {
if (!$enabled) {
$this->appConfig->deleteKey(Application::APP_ID, self::CONFIG_KEY_LEVEL);
}
}

public function getLevel(): DocMdpLevel {
$level = $this->appConfig->getValueInt(Application::APP_ID, self::CONFIG_KEY_LEVEL, DocMdpLevel::NOT_CERTIFIED->value);
return DocMdpLevel::from($level);
}

public function setLevel(DocMdpLevel $level): void {
$this->appConfig->setValueInt(Application::APP_ID, self::CONFIG_KEY_LEVEL, $level->value);
}

public function getConfig(): array {
return [
'enabled' => $this->isEnabled(),
'defaultLevel' => $this->getLevel()->value,
'availableLevels' => $this->getAvailableLevels(),
];
}

private function getAvailableLevels(): array {
return array_map(
fn (DocMdpLevel $level) => [
'value' => $level->value,
'label' => $level->getLabel($this->l10n),
'description' => $level->getDescription($this->l10n),
],
DocMdpLevel::cases()
);
}
}
Loading
Loading