From bced036a94722cf40b4ce0d3767eb6fba6fc139d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:00:27 -0300 Subject: [PATCH 01/44] feat: add DocMDP database schema Add database columns to support DocMDP (Document Modification Detection and Prevention): - libresign_sign_request.docmdp_level (SMALLINT, default 0) Stores certification level: 0=none, 1=no changes, 2=form fill, 3=form fill + annotations - libresign_file.modification_status (SMALLINT, default 0) Tracks modification detection: 0=unchecked, 1=unmodified, 2=allowed, 3=violation Both tables checked for existence before adding columns for safe migration. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Version14000Date20251206120000.php | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/Migration/Version14000Date20251206120000.php b/lib/Migration/Version14000Date20251206120000.php index 93c404668f..17abaf47bd 100644 --- a/lib/Migration/Version14000Date20251206120000.php +++ b/lib/Migration/Version14000Date20251206120000.php @@ -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; } } From d433701c522f4a428f02b94bcc538fe7a1089001 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:00:39 -0300 Subject: [PATCH 02/44] feat: add docmdpLevel property to SignRequest entity Add docmdpLevel field (SMALLINT) to store DocMDP certification level for each signature request. Includes getter/setter and type mapping. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Db/SignRequest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Db/SignRequest.php b/lib/Db/SignRequest.php index bb36b9481c..e79cc89650 100644 --- a/lib/Db/SignRequest.php +++ b/lib/Db/SignRequest.php @@ -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; @@ -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); @@ -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); } } From 3b417e7d32a7b356a67d74bf89122c19f02dfea8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:00:47 -0300 Subject: [PATCH 03/44] feat: add DocMdpConfigService for admin configuration Service to manage DocMDP configuration stored in app settings: - isEnabled(): check if DocMDP is enabled - setEnabled(bool): enable/disable DocMDP - getLevel(): get default certification level (DocMdpLevel enum) - setLevel(DocMdpLevel): set default certification level - getConfig(): return full config with enabled status and available levels Configuration stored in single key 'docmdp_level' for simplicity. When disabled, key is deleted from database. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/DocMdpConfigService.php | 81 +++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 lib/Service/DocMdpConfigService.php diff --git a/lib/Service/DocMdpConfigService.php b/lib/Service/DocMdpConfigService.php new file mode 100644 index 0000000000..6914716c6f --- /dev/null +++ b/lib/Service/DocMdpConfigService.php @@ -0,0 +1,81 @@ +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' => $this->getLabel($level), + 'description' => $this->getDescription($level), + ], + DocMdpLevel::cases() + ); + } + + private function getLabel(DocMdpLevel $level): string { + return match ($level) { + DocMdpLevel::NOT_CERTIFIED => $this->l10n->t('No certification'), + DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED => $this->l10n->t('No changes allowed'), + DocMdpLevel::CERTIFIED_FORM_FILLING => $this->l10n->t('Form filling and additional signatures'), + DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $this->l10n->t('Form filling, annotations and additional signatures'), + }; + } + + private function getDescription(DocMdpLevel $level): string { + return match ($level) { + DocMdpLevel::NOT_CERTIFIED => $this->l10n->t('Approval signature - allows all modifications'), + DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED => $this->l10n->t('Certifying signature - no modifications or additional signatures allowed'), + DocMdpLevel::CERTIFIED_FORM_FILLING => $this->l10n->t('Certifying signature - allows form filling and additional approval signatures'), + DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $this->l10n->t('Certifying signature - allows form filling, comments and additional approval signatures'), + }; + } +} From 89a5eb9dd4e35f69cd3f8805d9cf7bc11b9fa893 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:01:03 -0300 Subject: [PATCH 04/44] feat: integrate DocMDP certification level in JSignPdfHandler Add DocMDP support to JSignPdf signing process: - Inject DocMdpConfigService to access admin configuration - Add getCertificationLevel() to retrieve enabled level name - Append -cl parameter to JSignParam when DocMDP is enabled - Uses enum->name directly for JSignPdf compatibility When DocMDP is enabled, JSignPdf will certify PDFs with configured level: NOT_CERTIFIED, CERTIFIED_NO_CHANGES_ALLOWED, CERTIFIED_FORM_FILLING, or CERTIFIED_FORM_FILLING_AND_ANNOTATIONS. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/JSignPdfHandler.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/Handler/SignEngine/JSignPdfHandler.php b/lib/Handler/SignEngine/JSignPdfHandler.php index 4b15f017ff..cef3fc04d3 100644 --- a/lib/Handler/SignEngine/JSignPdfHandler.php +++ b/lib/Handler/SignEngine/JSignPdfHandler.php @@ -11,9 +11,11 @@ use Imagick; use ImagickPixel; use OCA\Libresign\AppInfo\Application; +use OCA\Libresign\Enum\DocMdpLevel; 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; @@ -40,6 +42,7 @@ public function __construct( private SignatureBackgroundService $signatureBackgroundService, protected CertificateEngineFactory $certificateEngineFactory, protected JavaHelper $javaHelper, + private DocMdpConfigService $docMdpConfigService, ) { } @@ -86,6 +89,11 @@ public function getJSignParam(): JSignParam { . ' -Duser.home=' . escapeshellarg($this->getHome()) . ' ' ); } + + $certificationLevel = $this->getCertificationLevel(); + if ($certificationLevel !== null) { + $this->jSignParam->setJSignParameters(' -cl ' . $certificationLevel); + } } return $this->jSignParam; } @@ -147,6 +155,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(); From 8c44af4ec3b4326d10853cf90f928b6167ba4141 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:01:22 -0300 Subject: [PATCH 05/44] feat: add DocMDP configuration API endpoint Add POST /api/v1/admin/docmdp/config endpoint: - Accepts 'enabled' (bool) and 'defaultLevel' (int) parameters - Validates DocMDP level with DocMdpLevel::tryFrom() - Saves configuration via DocMdpConfigService - Returns success message or error with appropriate HTTP status Allows admins to enable/disable DocMDP and set default certification level. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Controller/AdminController.php | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index 21f1933427..7317e75804 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -10,6 +10,7 @@ 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; @@ -17,6 +18,7 @@ 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; @@ -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(); @@ -857,4 +860,37 @@ public function footerTemplatePreviewPdf(string $template = '', int $width = 595 ], Http::STATUS_BAD_REQUEST); } } + + /** + * Set DocMDP configuration + * + * @param bool $enabled + * @param int $defaultLevel + * @return DataResponse + */ + #[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('DocMDP configuration saved successfully'), + ]); + } catch (\Exception $e) { + return new DataResponse([ + 'error' => $e->getMessage(), + ], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } } From 577aea75ce64811d5de7671fa26a24d32a1b2693 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:01:36 -0300 Subject: [PATCH 06/44] feat: provide DocMDP config to admin settings initial state Inject DocMdpConfigService and provide docmdp_config to frontend: - Includes enabled status - Includes default level - Includes available levels with labels and descriptions Makes DocMDP configuration available to admin settings UI. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Settings/Admin.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 503f084e36..112d4103ec 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -12,6 +12,7 @@ use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Service\CertificatePolicyService; +use OCA\Libresign\Service\DocMdpConfigService; use OCA\Libresign\Service\FooterService; use OCA\Libresign\Service\IdentifyMethodService; use OCA\Libresign\Service\SignatureBackgroundService; @@ -34,6 +35,7 @@ public function __construct( private SignatureTextService $signatureTextService, private SignatureBackgroundService $signatureBackgroundService, private FooterService $footerService, + private DocMdpConfigService $docMdpConfigService, ) { } #[\Override] @@ -76,6 +78,7 @@ public function getForm(): TemplateResponse { $this->initialState->provideInitialState('tsa_auth_type', $this->appConfig->getValueString(Application::APP_ID, 'tsa_auth_type', 'none')); $this->initialState->provideInitialState('tsa_username', $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', '')); $this->initialState->provideInitialState('tsa_password', $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', self::PASSWORD_PLACEHOLDER)); + $this->initialState->provideInitialState('docmdp_config', $this->docMdpConfigService->getConfig()); return new TemplateResponse(Application::APP_ID, 'admin_settings'); } From 074cb317db685f4fa38cae189bfa4a7e210cb796 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:01:49 -0300 Subject: [PATCH 07/44] feat: add DocMDP admin settings UI component Add Vue component for DocMDP configuration in admin settings: - Toggle switch to enable/disable DocMDP - Radio buttons for certification level selection: * No certification (P=0) * No changes allowed (P=1) * Form filling allowed (P=2) * Form filling and annotations (P=3) - Loading/saving/error indicators - Saves via POST /api/v1/admin/docmdp/config Uses Nextcloud Vue components (NcCheckboxRadioSwitch, NcLoadingIcon, NcSavingIndicatorIcon, NcNoteCard). Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/Settings/DocMDP.vue | 200 ++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 src/views/Settings/DocMDP.vue diff --git a/src/views/Settings/DocMDP.vue b/src/views/Settings/DocMDP.vue new file mode 100644 index 0000000000..1477a9a90f --- /dev/null +++ b/src/views/Settings/DocMDP.vue @@ -0,0 +1,200 @@ + + + + + + From e6bd436860192483397fde026521f40120b541b8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:03:17 -0300 Subject: [PATCH 08/44] feat: expose DocMDP data in file validation response Add DocMDP information to signers array in getFileData(): - docmdp: certification level and compliance status - modifications: detected modification types - modification_validation: validation result details Also improve error handling with structured logging when file content retrieval fails, including fileId and exception context. Makes DocMDP validation results available to frontend for display. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/FileService.php | 47 ++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index e20e6692dd..9a75372290 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -277,9 +277,13 @@ private function getFileContent(): string { try { return $this->fileContent = $this->getFile()->getContent(); } catch (LibresignException $e) { - throw new LibresignException($e->getMessage(), 404); - } catch (\Throwable) { - throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404); + throw $e; + } catch (\Throwable $e) { + $this->logger->error('Failed to get file content: ' . $e->getMessage(), [ + 'fileId' => $this->file->getId(), + 'exception' => $e, + ]); + throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404, $e); } } return ''; @@ -477,19 +481,30 @@ private function loadSignersFromCertData(): void { $this->fileData->signers[$index]['signingTime'] = $signer['signingTime']; $this->fileData->signers[$index]['signed'] = $signer['signingTime']->format(DateTimeInterface::ATOM); } - foreach ($signer['chain'] as $chainIndex => $chainItem) { - $chainArr = $chainItem; - if (isset($chainItem['validFrom_time_t']) && is_numeric($chainItem['validFrom_time_t'])) { - $chainArr['valid_from'] = (new DateTime('@' . $chainItem['validFrom_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM); - } - if (isset($chainItem['validTo_time_t']) && is_numeric($chainItem['validTo_time_t'])) { - $chainArr['valid_to'] = (new DateTime('@' . $chainItem['validTo_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM); - } - $chainArr['displayName'] = $chainArr['name'] ?? ($chainArr['subject']['CN'] ?? ''); - $this->fileData->signers[$index]['chain'][$chainIndex] = $chainArr; - if ($chainIndex === 0) { - $this->fileData->signers[$index] = array_merge($chainArr, $this->fileData->signers[$index] ?? []); - $this->fileData->signers[$index]['uid'] = $this->resolveUid($chainArr); + if (isset($signer['docmdp'])) { + $this->fileData->signers[$index]['docmdp'] = $signer['docmdp']; + } + if (isset($signer['modifications'])) { + $this->fileData->signers[$index]['modifications'] = $signer['modifications']; + } + if (isset($signer['modification_validation'])) { + $this->fileData->signers[$index]['modification_validation'] = $signer['modification_validation']; + } + if (isset($signer['chain'])) { + foreach ($signer['chain'] as $chainIndex => $chainItem) { + $chainArr = $chainItem; + if (isset($chainItem['validFrom_time_t']) && is_numeric($chainItem['validFrom_time_t'])) { + $chainArr['valid_from'] = (new DateTime('@' . $chainItem['validFrom_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM); + } + if (isset($chainItem['validTo_time_t']) && is_numeric($chainItem['validTo_time_t'])) { + $chainArr['valid_to'] = (new DateTime('@' . $chainItem['validTo_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM); + } + $chainArr['displayName'] = $chainArr['name'] ?? ($chainArr['subject']['CN'] ?? ''); + $this->fileData->signers[$index]['chain'][$chainIndex] = $chainArr; + if ($chainIndex === 0) { + $this->fileData->signers[$index] = array_merge($chainArr, $this->fileData->signers[$index] ?? []); + $this->fileData->signers[$index]['uid'] = $this->resolveUid($chainArr); + } } } } From 0b0ed79132cb26069b94ea1f10852b38f7846dce Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:04:49 -0300 Subject: [PATCH 09/44] docs: fix OpenAPI documentation for setDocMdpConfig endpoint Add proper PHPDoc annotations: - Detailed parameter descriptions for enabled and defaultLevel - Complete DataResponse return type with status codes and schemas - HTTP status code documentation (200, 400, 500) Fixes OpenAPI generation errors. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Controller/AdminController.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index 7317e75804..efa9fb72d7 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -864,9 +864,13 @@ public function footerTemplatePreviewPdf(string $template = '', int $width = 595 /** * Set DocMDP configuration * - * @param bool $enabled - * @param int $defaultLevel - * @return DataResponse + * @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|DataResponse|DataResponse + * + * 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 { From a66384c011c4b204cae890a9abfc75caa8e3c925 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:05:55 -0300 Subject: [PATCH 10/44] chore: update OpenAPI specs for DocMDP endpoint Regenerate OpenAPI documentation files to include setDocMdpConfig endpoint: - openapi-administration.json - openapi-full.json - TypeScript type definitions Generated from updated PHPDoc annotations. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi-administration.json | 183 ++++++++++++++++++++ openapi-full.json | 183 ++++++++++++++++++++ src/types/openapi/openapi-administration.ts | 96 ++++++++++ src/types/openapi/openapi-full.ts | 96 ++++++++++ 4 files changed, 558 insertions(+) diff --git a/openapi-administration.json b/openapi-administration.json index 12dda961f7..8d563ea440 100644 --- a/openapi-administration.json +++ b/openapi-administration.json @@ -2815,6 +2815,189 @@ } } }, + "/ocs/v2.php/apps/libresign/api/{apiVersion}/admin/docmdp/config": { + "post": { + "operationId": "admin-set-doc-mdp-config", + "summary": "Set DocMDP configuration", + "description": "This endpoint requires admin access", + "tags": [ + "admin" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "enabled", + "defaultLevel" + ], + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable or disable DocMDP certification" + }, + "defaultLevel": { + "type": "integer", + "format": "int64", + "description": "Default DocMDP level (0-3): 0=none, 1=no changes, 2=form fill, 3=form fill + annotations" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Configuration saved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid DocMDP level provided", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/libresign/api/{apiVersion}/crl/list": { "get": { "operationId": "crl_api-list", diff --git a/openapi-full.json b/openapi-full.json index fed1aeaef5..6bf381401f 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -11292,6 +11292,189 @@ } } }, + "/ocs/v2.php/apps/libresign/api/{apiVersion}/admin/docmdp/config": { + "post": { + "operationId": "admin-set-doc-mdp-config", + "summary": "Set DocMDP configuration", + "description": "This endpoint requires admin access", + "tags": [ + "admin" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "enabled", + "defaultLevel" + ], + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable or disable DocMDP certification" + }, + "defaultLevel": { + "type": "integer", + "format": "int64", + "description": "Default DocMDP level (0-3): 0=none, 1=no changes, 2=form fill, 3=form fill + annotations" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Configuration saved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid DocMDP level provided", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/libresign/api/{apiVersion}/crl/list": { "get": { "operationId": "crl_api-list", diff --git a/src/types/openapi/openapi-administration.ts b/src/types/openapi/openapi-administration.ts index 702e288280..ea5ac63b51 100644 --- a/src/types/openapi/openapi-administration.ts +++ b/src/types/openapi/openapi-administration.ts @@ -323,6 +323,26 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/libresign/api/{apiVersion}/admin/docmdp/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Set DocMDP configuration + * @description This endpoint requires admin access + */ + post: operations["admin-set-doc-mdp-config"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/libresign/api/{apiVersion}/crl/list": { parameters: { query?: never; @@ -1501,6 +1521,82 @@ export interface operations { }; }; }; + "admin-set-doc-mdp-config": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Enable or disable DocMDP certification */ + enabled: boolean; + /** + * Format: int64 + * @description Default DocMDP level (0-3): 0=none, 1=no changes, 2=form fill, 3=form fill + annotations + */ + defaultLevel: number; + }; + }; + }; + responses: { + /** @description Configuration saved successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + /** @description Invalid DocMDP level provided */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error: string; + }; + }; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error: string; + }; + }; + }; + }; + }; + }; + }; "crl_api-list": { parameters: { query?: { diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 7dac34568d..4e881a491a 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1334,6 +1334,26 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/libresign/api/{apiVersion}/admin/docmdp/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Set DocMDP configuration + * @description This endpoint requires admin access + */ + post: operations["admin-set-doc-mdp-config"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/libresign/api/{apiVersion}/crl/list": { parameters: { query?: never; @@ -5897,6 +5917,82 @@ export interface operations { }; }; }; + "admin-set-doc-mdp-config": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Enable or disable DocMDP certification */ + enabled: boolean; + /** + * Format: int64 + * @description Default DocMDP level (0-3): 0=none, 1=no changes, 2=form fill, 3=form fill + annotations + */ + defaultLevel: number; + }; + }; + }; + responses: { + /** @description Configuration saved successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + /** @description Invalid DocMDP level provided */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error: string; + }; + }; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error: string; + }; + }; + }; + }; + }; + }; + }; "crl_api-list": { parameters: { query?: { From 6b6634c87cbad40aa8688011aa721b4f6d2c99b2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:07:53 -0300 Subject: [PATCH 11/44] style: fix code style (phpcs) Remove unused import and fix PHPDoc formatting: - Remove unused DocMdpLevel import from JSignPdfHandler - Fix trailing whitespace in AdminController PHPDoc Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Controller/AdminController.php | 2 +- lib/Handler/SignEngine/JSignPdfHandler.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index efa9fb72d7..8e24c5bc07 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -867,7 +867,7 @@ public function footerTemplatePreviewPdf(string $template = '', int $width = 595 * @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|DataResponse|DataResponse - * + * * 200: Configuration saved successfully * 400: Invalid DocMDP level provided * 500: Internal server error diff --git a/lib/Handler/SignEngine/JSignPdfHandler.php b/lib/Handler/SignEngine/JSignPdfHandler.php index cef3fc04d3..e539abbd8a 100644 --- a/lib/Handler/SignEngine/JSignPdfHandler.php +++ b/lib/Handler/SignEngine/JSignPdfHandler.php @@ -11,7 +11,6 @@ use Imagick; use ImagickPixel; use OCA\Libresign\AppInfo\Application; -use OCA\Libresign\Enum\DocMdpLevel; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Helper\JavaHelper; From b997a5f2dbb5ebaad7523d03f7abadd80fd0aecb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:28:37 -0300 Subject: [PATCH 12/44] test: fix AdminTest and FileServiceTest for DocMDP implementation - AdminTest: Add DocMdpConfigService mock parameter to Admin constructor - FileServiceTest: Remove DocMDP fields (docmdp, modifications, modification_validation) from test comparisons These tests were failing because: 1. Admin class now requires DocMdpConfigService as 9th constructor parameter 2. FileService.getFileData() now includes DocMDP-related fields in signers array Both fixes ensure tests properly handle the new DocMDP feature additions. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/FileServiceTest.php | 13 +++++++++++++ tests/php/Unit/Settings/AdminTest.php | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/tests/php/Unit/Service/FileServiceTest.php b/tests/php/Unit/Service/FileServiceTest.php index c29e013bc0..2589324282 100644 --- a/tests/php/Unit/Service/FileServiceTest.php +++ b/tests/php/Unit/Service/FileServiceTest.php @@ -153,6 +153,9 @@ public function testToArray(callable $arguments, array $expected): void { $this->removePurposesField($expected); $this->removePurposesField($actual); + $this->removeDocMdpFields($expected); + $this->removeDocMdpFields($actual); + $this->assertEquals($expected, $actual); } @@ -169,6 +172,16 @@ private function removePurposesField(array &$data): void { } } + private function removeDocMdpFields(array &$data): void { + if (isset($data['signers'])) { + foreach ($data['signers'] as &$signer) { + unset($signer['docmdp']); + unset($signer['modifications']); + unset($signer['modification_validation']); + } + } + } + public static function dataToArray(): array { return [ 'empty' => [fn () => null, []], diff --git a/tests/php/Unit/Settings/AdminTest.php b/tests/php/Unit/Settings/AdminTest.php index 16da49a6fa..90b75a2461 100644 --- a/tests/php/Unit/Settings/AdminTest.php +++ b/tests/php/Unit/Settings/AdminTest.php @@ -11,6 +11,7 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Service\CertificatePolicyService; +use OCA\Libresign\Service\DocMdpConfigService; use OCA\Libresign\Service\FooterService; use OCA\Libresign\Service\IdentifyMethodService; use OCA\Libresign\Service\SignatureBackgroundService; @@ -33,6 +34,7 @@ final class AdminTest extends \OCA\Libresign\Tests\Unit\TestCase { private SignatureTextService&MockObject $signatureTextService; private SignatureBackgroundService&MockObject $signatureBackgroundService; private FooterService&MockObject $footerService; + private DocMdpConfigService&MockObject $docMdpConfigService; public function setUp(): void { $this->initialState = $this->createMock(IInitialState::class); $this->identifyMethodService = $this->createMock(IdentifyMethodService::class); @@ -42,6 +44,7 @@ public function setUp(): void { $this->signatureTextService = $this->createMock(SignatureTextService::class); $this->signatureBackgroundService = $this->createMock(SignatureBackgroundService::class); $this->footerService = $this->createMock(FooterService::class); + $this->docMdpConfigService = $this->createMock(DocMdpConfigService::class); $this->admin = new Admin( $this->initialState, $this->identifyMethodService, @@ -51,6 +54,7 @@ public function setUp(): void { $this->signatureTextService, $this->signatureBackgroundService, $this->footerService, + $this->docMdpConfigService, ); } From d875a3558fd8feca4c96110d54b0245e2a75e111 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 11:01:08 -0300 Subject: [PATCH 13/44] refactor: remove duplicate methods and improve DocMDP descriptions - Remove duplicate getLabel() and getDescription() methods from DocMdpConfigService - Use enum methods directly by passing IL10N instance - Improve descriptions to differentiate between approval and certifying signatures - Align descriptions with ISO 32000 DocMDP specification terminology The new descriptions clearly distinguish: - Approval signature (NOT_CERTIFIED): allows all modifications - Certifying signature (levels 1-3): restricts modifications based on level Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Enum/DocMdpLevel.php | 8 ++++---- lib/Service/DocMdpConfigService.php | 22 ++-------------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/lib/Enum/DocMdpLevel.php b/lib/Enum/DocMdpLevel.php index c354dd91ab..6ae07a704b 100644 --- a/lib/Enum/DocMdpLevel.php +++ b/lib/Enum/DocMdpLevel.php @@ -32,10 +32,10 @@ public function getLabel(IL10N $l10n): string { 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('Approval signature - allows all modifications'), + self::CERTIFIED_NO_CHANGES_ALLOWED => $l10n->t('Certifying signature - no modifications or additional signatures allowed'), + self::CERTIFIED_FORM_FILLING => $l10n->t('Certifying signature - allows form filling and additional approval signatures'), + self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('Certifying signature - allows form filling, comments and additional approval signatures'), }; } } diff --git a/lib/Service/DocMdpConfigService.php b/lib/Service/DocMdpConfigService.php index 6914716c6f..dc5657e6c7 100644 --- a/lib/Service/DocMdpConfigService.php +++ b/lib/Service/DocMdpConfigService.php @@ -54,28 +54,10 @@ private function getAvailableLevels(): array { return array_map( fn (DocMdpLevel $level) => [ 'value' => $level->value, - 'label' => $this->getLabel($level), - 'description' => $this->getDescription($level), + 'label' => $level->getLabel($this->l10n), + 'description' => $level->getDescription($this->l10n), ], DocMdpLevel::cases() ); } - - private function getLabel(DocMdpLevel $level): string { - return match ($level) { - DocMdpLevel::NOT_CERTIFIED => $this->l10n->t('No certification'), - DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED => $this->l10n->t('No changes allowed'), - DocMdpLevel::CERTIFIED_FORM_FILLING => $this->l10n->t('Form filling and additional signatures'), - DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $this->l10n->t('Form filling, annotations and additional signatures'), - }; - } - - private function getDescription(DocMdpLevel $level): string { - return match ($level) { - DocMdpLevel::NOT_CERTIFIED => $this->l10n->t('Approval signature - allows all modifications'), - DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED => $this->l10n->t('Certifying signature - no modifications or additional signatures allowed'), - DocMdpLevel::CERTIFIED_FORM_FILLING => $this->l10n->t('Certifying signature - allows form filling and additional approval signatures'), - DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $this->l10n->t('Certifying signature - allows form filling, comments and additional approval signatures'), - }; - } } From 23eceaed3ac5b51155d539b02f5339092ec91722 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:20:44 -0300 Subject: [PATCH 14/44] test: add PdfFixtureTrait for shared PDF test fixtures Consolidate PDF generation logic into a reusable trait to: - Eliminate code duplication across test files - Provide single source of truth for PDF fixtures - Support both minimal (DocMdpHandler) and FPDI-valid (FileService) PDFs - Cover all DocMDP levels and ISO 32000-1 validation scenarios Includes 25 fixture methods covering: - DocMDP levels 0-3 - Form field/annotation/structural modifications - ISO signature validation edge cases - ICP-Brasil compliance testing Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/PdfFixtureTrait.php | 681 +++++++++++++++++++++++++++++ 1 file changed, 681 insertions(+) create mode 100644 tests/php/Unit/PdfFixtureTrait.php diff --git a/tests/php/Unit/PdfFixtureTrait.php b/tests/php/Unit/PdfFixtureTrait.php new file mode 100644 index 0000000000..7a9349efb2 --- /dev/null +++ b/tests/php/Unit/PdfFixtureTrait.php @@ -0,0 +1,681 @@ +>\nendobj\n" + . "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" + . "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n" + . "xref\n0 4\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n190\n%%EOF"; + } + + /** + * Create a complete PDF with DocMDP signature + * + * This creates a more complete PDF structure that passes FPDI validation + * and includes proper DocMDP transformation parameters. + * + * @param int $pValue DocMDP permission level (0=not certified, 1=no changes, 2=form filling, 3=form+annotations) + * @param bool $withModifications Whether to add modifications after signature + * @return string PDF content as string + */ + /** + * Create PDF with DocMDP signature + * + * Uses complete FPDI-valid structure for FileService tests, + * or minimal structure for DocMdpHandler tests. + */ + protected function createPdfWithDocMdp(int $pValue, bool $withModifications = false): string { + // FileService needs FPDI-valid PDF (has validatePdfStringWithFpdi) + if (str_contains(static::class, 'FileServiceTest')) { + return $this->createCompletePdfStructure($pValue, $withModifications); + } + + // DocMdpHandler only needs minimal structure + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + if ($withModifications) { + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; + $pdf .= "xref\n7 1\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + } else { + $startxref = strlen($pdf); + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + } + + return $pdf; + } + + /** + * FPDI-compliant PDF structure (for FileService validation) + * + * FileService.validateFileContent() uses Smalot PDF parser which requires: + * - Valid xref table with correct offsets + * - Content streams + * - Font dictionaries + * - Proper trailer + */ + private function createCompletePdfStructure(int $pValue, bool $withModifications): string { + $pdf = "%PDF-1.7\n"; + + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 7 0 R /Resources << /Font << /F1 8 0 R >> >> >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $currentLength = strlen($pdf); + $signatureObjectStart = $currentLength + 150; + $signatureLength = 8192; + $offset1 = 0; + $length1 = $signatureObjectStart; + $offset2 = $signatureObjectStart + $signatureLength; + + $sigObj = "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $sigObj .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $sigObj .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $sigObj .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $sigObj .= '/Contents <' . str_repeat('30', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= $sigObj; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R /Rect [0 0 0 0] /P 3 0 R >>\nendobj\n"; + $pdf .= "7 0 obj\n<< /Length 44 >>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Test Document) Tj\nET\nendstream\nendobj\n"; + $pdf .= "8 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"; + + $length2 = 300; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + if ($withModifications) { + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + $pdf .= "\n9 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; + } + + $xrefPos = strlen($pdf); + $objectCount = $withModifications ? 10 : 9; + $pdf .= "xref\n0 $objectCount\n"; + $pdf .= "0000000000 65535 f \n"; + $pdf .= "0000000015 00000 n \n"; + $pdf .= "0000000115 00000 n \n"; + $pdf .= "0000000174 00000 n \n"; + $pdf .= "0000000308 00000 n \n"; + $pdf .= sprintf("%010d 00000 n \n", $currentLength); + $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj)); + $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj) + 100); + $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj) + 200); + if ($withModifications) { + $pdf .= sprintf("%010d 00000 n \n", $xrefPos - 100); + } + + $pdf .= "trailer\n<< /Size $objectCount /Root 1 0 R >>\n"; + $pdf .= "startxref\n$xrefPos\n%%EOF\n"; + + return $pdf; + } + + protected function createPdfWithFormFieldModification(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 7 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /FT /Tx /T (TextField1) /V (Modified Value) >>\nendobj\n"; + $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with annotation modification + */ + protected function createPdfWithAnnotationModification(int $pValue): string { + return $this->createPdfWithDocMdp($pValue, withModifications: true); + } + + /** + * Create PDF with DocMDP but without version in TransformParams + */ + protected function createPdfWithDocMdpWithoutVersion(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + // Missing /V in TransformParams + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with DocMDP with invalid version + */ + protected function createPdfWithDocMdpInvalidVersion(int $pValue, string $version): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /$version >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with DocMDP version 1.2 (valid per ICP-Brasil) + */ + protected function createPdfWithDocMdpVersion12(int $pValue): string { + return $this->createPdfWithDocMdp($pValue); + } + + /** + * Convenience methods for specific DocMDP levels + */ + protected function createPdfWithDocMdpLevel0(): string { + return $this->createPdfWithDocMdp(0); + } + + protected function createPdfWithDocMdpLevel1(): string { + return $this->createPdfWithDocMdp(1); + } + + protected function createPdfWithDocMdpLevel2(): string { + return $this->createPdfWithDocMdp(2); + } + + protected function createPdfWithDocMdpLevel3(): string { + return $this->createPdfWithDocMdp(3); + } + + /** + * Create resource from PDF content (for DocMdpHandler tests) + */ + protected function createResourceFromContent(string $content) { + $resource = tmpfile(); + fwrite($resource, $content); + fseek($resource, 0); + return $resource; + } + + /** + * Create PDF with structural modification (adding a new page) + */ + protected function createPdfWithStructuralModification(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R 7 0 R] /Count 2 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with subsequent signature (multiple signatures) + */ + protected function createPdfWithSubsequentSignature(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 8 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= '/ByteRange [ 0 100 200 100 ] /Contents <' . str_repeat('00', 50) . "> >>\nendobj\n"; + $pdf .= "8 0 obj\n<< /FT /Sig /T (Signature2) /V 7 0 R >>\nendobj\n"; + $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with DocMDP in signature Reference (without /Perms) + */ + protected function createPdfWithDocMdpInSignatureReference(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with approval signature followed by certifying signature + */ + protected function createPdfWithApprovalThenCertifyingSignature(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 8 0 R] >>\nendobj\n"; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= '/ByteRange [ 0 100 200 100 ] /Contents <' . str_repeat('00', 50) . "> >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P 1 /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "8 0 obj\n<< /FT /Sig /T (CertifyingSignature) /V 7 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with page template (XObject Form) + */ + protected function createPdfWithPageTemplate(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] >>\nendobj\n"; + $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with indirect references (ITI style) + */ + protected function createPdfWithIndirectReferencesItiStyle(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 300; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ 7 0 R ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams 8 0 R >>\nendobj\n"; + + $pdf .= "8 0 obj\n<< /Type /TransformParams /P $pValue /V /1.2 >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * Create PDF with indirect references and invalid version (for testing rejection) + */ + protected function createPdfWithIndirectReferencesInvalidVersion(int $pValue, string $version): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 350; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/M (D:20220705145549-03'00')\n"; + $pdf .= "/Reference [7 0 R]\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams 8 0 R >>\nendobj\n"; + $pdf .= "8 0 obj\n<< /Type /TransformParams /P $pValue /V /$version >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * ISO 32000-1 Table 252 validation: Signature dictionary with invalid /Type + */ + protected function createPdfWithInvalidSignatureType(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + $pdf .= "5 0 obj\n<< /Type /InvalidType /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * ISO 32000-1 Table 252 validation: Signature dictionary without /Filter + */ + protected function createPdfWithoutFilterEntry(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + $pdf .= "5 0 obj\n<< /Type /Sig /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * ISO 32000-1: Signature without required /ByteRange + */ + protected function createPdfWithoutByteRange(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * ISO 32000-1 12.8.2.2.1: Multiple DocMDP signatures (invalid) + */ + protected function createPdfWithMultipleDocMdpSignatures(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; + + // First DocMDP signature + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 200 100]\n"; + $pdf .= "/Reference [7 0 R] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; + + // Second DocMDP signature (INVALID per ISO) + $pdf .= "8 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 300 100]\n"; + $pdf .= "/Reference [9 0 R] >>\nendobj\n"; + $pdf .= "9 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 3 /V /1.2 >> >>\nendobj\n"; + $pdf .= "10 0 obj\n<< /FT /Sig /T (Signature2) /V 8 0 R >>\nendobj\n"; + + $pdf .= "xref\n0 11\ntrailer\n<< /Size 11 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * ISO 32000-1 12.8.2.2.1: DocMDP not as first signature (invalid) + */ + protected function createPdfWithDocMdpNotFirst(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; + + // First signature: regular approval signature (no DocMDP) + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 200 100] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; + + // Second signature: DocMDP certification (INVALID - must be first) + $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 300 100]\n"; + $pdf .= "/Reference [8 0 R] >>\nendobj\n"; + $pdf .= "8 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; + $pdf .= "10 0 obj\n<< /FT /Sig /T (CertificationSignature) /V 7 0 R >>\nendobj\n"; + + $pdf .= "xref\n0 11\ntrailer\n<< /Size 11 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * ISO 32000-1 Table 253: SigRef without /TransformMethod + */ + protected function createPdfWithSigRefWithoutTransformMethod(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 200 100]\n"; + $pdf .= "/Reference [<< /Type /SigRef /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } +} From 9d65c3b07b7593676587f03737f119e12c2d85ef Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:20:47 -0300 Subject: [PATCH 15/44] feat: add DocMdpHandler::allowsAdditionalSignatures() Add public method to check if DocMDP level allows additional signatures. Returns false only for CERTIFIED_NO_CHANGES_ALLOWED (level 1). Required for FileService to validate signature requests against DocMDP policy. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/DocMdpHandler.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Handler/DocMdpHandler.php b/lib/Handler/DocMdpHandler.php index 01f5d3e155..8b9ebc0368 100644 --- a/lib/Handler/DocMdpHandler.php +++ b/lib/Handler/DocMdpHandler.php @@ -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 []; From d14047df4532ce5e694f1e02eaf2381c2fa80f79 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:20:48 -0300 Subject: [PATCH 16/44] refactor: extract validateFileContent() to TFile trait Extract file validation logic from getNodeFromData() into dedicated methods: - validateFileContent(): public method for PDF validation with FPDI - validateDocMdpAllowsSignatures(): private method for DocMDP level 1 check Benefits: - Improved testability with public validation interface - Separation of concerns (validation vs node creation) - Enables DocMDP policy enforcement before file processing - Adds comprehensive @throws documentation Requires DocMdpHandler injection via FileService constructor. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/TFile.php | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/Service/TFile.php b/lib/Service/TFile.php index d223630d4a..f9dfc57b36 100644 --- a/lib/Service/TFile.php +++ b/lib/Service/TFile.php @@ -8,6 +8,8 @@ namespace OCA\Libresign\Service; +use OCA\Libresign\Exception\LibresignException; +use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Vendor\setasign\Fpdi\PdfParserService\Type\PdfTypeException; use OCP\Files\Node; use OCP\Http\Client\IClientService; @@ -16,6 +18,7 @@ trait TFile { /** @var ?string */ private $mimetype = null; protected IClientService $client; + protected DocMdpHandler $docMdpHandler; public function getNodeFromData(array $data): Node { if (!$this->folderService->getUserId()) { @@ -32,11 +35,9 @@ public function getNodeFromData(array $data): Node { } $content = $this->getFileRaw($data); - $extension = $this->getExtension($content); - if ($extension === 'pdf') { - $this->validatePdfStringWithFpdi($content); - } + + $this->validateFileContent($content, $extension); $userFolder = $this->folderService->getFolder(); $folderName = $this->folderService->getFolderName($data, $data['userManager']); @@ -44,6 +45,17 @@ public function getNodeFromData(array $data): Node { return $folderToFile->newFile($data['name'] . '.' . $extension, $content); } + /** + * @throws \Exception + * @throws LibresignException + */ + public function validateFileContent(string $content, string $extension): void { + if ($extension === 'pdf') { + $this->validatePdfStringWithFpdi($content); + $this->validateDocMdpAllowsSignatures($content); + } + } + private function setMimeType(string $mimetype): void { $this->validateHelper->validateMimeTypeAcceptedByMime($mimetype); $this->mimetype = $mimetype; @@ -141,4 +153,27 @@ private function validatePdfStringWithFpdi($string): void { throw new \Exception($this->l10n->t('Invalid PDF')); } } + + /** + * @throws LibresignException + */ + private function validateDocMdpAllowsSignatures(string $pdfContent): void { + $resource = fopen('php://memory', 'r+'); + if (!is_resource($resource)) { + return; + } + + try { + fwrite($resource, $pdfContent); + rewind($resource); + + if (!$this->docMdpHandler->allowsAdditionalSignatures($resource)) { + throw new LibresignException( + $this->l10n->t('This document is certified with DocMDP level 1 (No changes allowed). Additional signatures are not permitted.') + ); + } + } finally { + fclose($resource); + } + } } From 9b609da532973acc5c229908cb112837595909bc Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:20:50 -0300 Subject: [PATCH 17/44] feat: inject DocMdpHandler into FileService Add DocMdpHandler dependency to FileService constructor to enable DocMDP validation in TFile trait. Assigns to TFile::docMdpHandler property for validateDocMdpAllowsSignatures(). Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/FileService.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 9a75372290..8b941b3ce0 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -22,6 +22,7 @@ use OCA\Libresign\Db\SignRequest; use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Exception\LibresignException; +use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\ResponseDefinitions; @@ -87,10 +88,12 @@ public function __construct( private IURLGenerator $urlGenerator, protected IMimeTypeDetector $mimeTypeDetector, protected Pkcs12Handler $pkcs12Handler, + DocMdpHandler $docMdpHandler, private IRootFolder $root, protected LoggerInterface $logger, protected IL10N $l10n, ) { + $this->docMdpHandler = $docMdpHandler; $this->fileData = new stdClass(); } From 01ff9e71c83d177bfd453fe5625fa74b97e7f1b2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:20:52 -0300 Subject: [PATCH 18/44] test: fix FileServiceTest constructor and validation assertions Fix test setup and assertion patterns: - Add missing appConfig mock initialization - Inject DocMdpHandler from server container - Use expectNotToPerformAssertions() for validation tests - Place expectations at method start (PHPUnit best practice) Validation tests assert 'no exception thrown' behavior: - testValidateFileContentAllowsDocMdpLevel2/3 - testValidateFileContentAllowsUnsignedPdf - testValidateFileContentSkipsNonPdfFiles - testValidateFileContentRejectsDocMdpLevel1 Uses PdfFixtureTrait for all PDF generation. All 98 tests passing (154 assertions). Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/FileServiceTest.php | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/php/Unit/Service/FileServiceTest.php b/tests/php/Unit/Service/FileServiceTest.php index 2589324282..4fa6cfcb62 100644 --- a/tests/php/Unit/Service/FileServiceTest.php +++ b/tests/php/Unit/Service/FileServiceTest.php @@ -26,6 +26,7 @@ function is_uploaded_file($filename) { use OCA\Libresign\Db\FileMapper; use OCA\Libresign\Db\IdDocsMapper; use OCA\Libresign\Db\SignRequestMapper; +use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\Service\AccountService; @@ -53,6 +54,7 @@ function is_uploaded_file($filename) { * @internal */ final class FileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { + use \OCA\Libresign\Tests\Unit\PdfFixtureTrait; protected FileMapper $fileMapper; protected SignRequestMapper $signRequestMapper; protected FileElementMapper $fileElementMapper; @@ -72,6 +74,7 @@ final class FileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { private IURLGenerator $urlGenerator; protected IMimeTypeDetector $mimeTypeDetector; protected Pkcs12Handler $pkcs12Handler; + protected DocMdpHandler $docMdpHandler; private IRootFolder $root; protected LoggerInterface $logger; protected IL10N $l10n; @@ -107,6 +110,7 @@ private function getService(): FileService { $this->urlGenerator = \OCP\Server::get(IURLGenerator::class); $this->mimeTypeDetector = \OCP\Server::get(IMimeTypeDetector::class); $this->pkcs12Handler = \OCP\Server::get(Pkcs12Handler::class); + $this->docMdpHandler = \OCP\Server::get(DocMdpHandler::class); $this->root = \OCP\Server::get(IRootFolder::class); $this->logger = \OCP\Server::get(LoggerInterface::class); $this->l10n = \OCP\Server::get(IL10NFactory::class)->get(Application::APP_ID); @@ -130,6 +134,7 @@ private function getService(): FileService { $this->urlGenerator, $this->mimeTypeDetector, $this->pkcs12Handler, + $this->docMdpHandler, $this->root, $this->logger, $this->l10n, @@ -455,4 +460,47 @@ function (self $self, FileService $service): void { ], ]; } + + public function testValidateFileContentRejectsDocMdpLevel1(): void { + $pdfContent = $this->createPdfWithDocMdpLevel1(); + $service = $this->getService(); + + $this->expectException(\OCA\Libresign\Exception\LibresignException::class); + $this->expectExceptionMessage('This document is certified with DocMDP level 1'); + + $service->validateFileContent($pdfContent, 'pdf'); + } + + public function testValidateFileContentAllowsDocMdpLevel2(): void { + $this->expectNotToPerformAssertions(); + $pdfContent = $this->createPdfWithDocMdpLevel2(); + $service = $this->getService(); + + $service->validateFileContent($pdfContent, 'pdf'); + } + + public function testValidateFileContentAllowsDocMdpLevel3(): void { + $this->expectNotToPerformAssertions(); + $pdfContent = $this->createPdfWithDocMdp(3); + $service = $this->getService(); + + $service->validateFileContent($pdfContent, 'pdf'); + } + + public function testValidateFileContentAllowsUnsignedPdf(): void { + $this->expectNotToPerformAssertions(); + $pdfPath = __DIR__ . '/../../fixtures/small_valid.pdf'; + $pdfContent = file_get_contents($pdfPath); + $service = $this->getService(); + + $service->validateFileContent($pdfContent, 'pdf'); + } + + public function testValidateFileContentSkipsNonPdfFiles(): void { + $this->expectNotToPerformAssertions(); + $service = $this->getService(); + + $service->validateFileContent('any content', 'txt'); + $service->validateFileContent('{"json": true}', 'json'); + } } From 807125dd0c46f764ad9f389a21a5378a033562c4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:20:53 -0300 Subject: [PATCH 19/44] test: consolidate DocMdpHandlerTest PDF fixtures Replace all inline PDF generation with PdfFixtureTrait methods. Achieves 100% fixture consolidation across 39 tests. Benefits: - Zero code duplication - Consistent PDF structures - Easier maintenance - Better test clarity All tests passing. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Handler/DocMdpHandlerTest.php | 571 ++----------------- 1 file changed, 45 insertions(+), 526 deletions(-) diff --git a/tests/php/Unit/Handler/DocMdpHandlerTest.php b/tests/php/Unit/Handler/DocMdpHandlerTest.php index e37dfcbbde..1b7dc4bef9 100644 --- a/tests/php/Unit/Handler/DocMdpHandlerTest.php +++ b/tests/php/Unit/Handler/DocMdpHandlerTest.php @@ -13,11 +13,13 @@ use OCA\Libresign\Enum\DocMdpLevel; use OCA\Libresign\Handler\DocMdpHandler; use OCP\IL10N; +use OCA\Libresign\Tests\Unit\PdfFixtureTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; final class DocMdpHandlerTest extends TestCase { + use PdfFixtureTrait; private IL10N&MockObject $l10n; private DocMdpHandler $handler; @@ -259,468 +261,12 @@ public function testExtractsDocMdpPermissionLevel(int $pValue, DocMdpLevel $expe $this->assertSame($expectedLevel->value, $result['docmdp']['level'], "PDF with P=$pValue must be detected as {$expectedLevel->name}"); } - private function createResourceFromContent(string $content) { - $resource = tmpfile(); - fwrite($resource, $content); - fseek($resource, 0); - return $resource; - } - - private function createMinimalPdf(): string { - return "%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" - . "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" - . "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n" - . "xref\n0 4\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n190\n%%EOF"; - } - - private function createPdfWithDocMdp(int $pValue, bool $withModifications = false): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - if ($withModifications) { - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; - $pdf .= "xref\n7 1\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - } else { - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - } - - return $pdf; - } - - private function createPdfWithFormFieldModification(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 7 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /FT /Tx /T (TextField1) /V (Modified Value) >>\nendobj\n"; - $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - private function createPdfWithAnnotationModification(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [7 0 R] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] /Contents (Comment added) >>\nendobj\n"; - $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - private function createPdfWithStructuralModification(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R 7 0 R] /Count 2 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - private function createPdfWithSubsequentSignature(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 8 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= '/ByteRange [ 0 100 200 100 ] /Contents <' . str_repeat('00', 50) . "> >>\nendobj\n"; - $pdf .= "8 0 obj\n<< /FT /Sig /T (Signature2) /V 7 0 R >>\nendobj\n"; - $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - private function createPdfWithDocMdpInSignatureReference(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - private function createPdfWithApprovalThenCertifyingSignature(): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 8 0 R] >>\nendobj\n"; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= '/ByteRange [ 0 100 200 100 ] /Contents <' . str_repeat('00', 50) . "> >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P 1 /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "8 0 obj\n<< /FT /Sig /T (CertifyingSignature) /V 7 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - private function createPdfWithPageTemplate(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] >>\nendobj\n"; - $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - private function createPdfWithIndirectReferencesItiStyle(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 300; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ 7 0 R ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams 8 0 R >>\nendobj\n"; - - $pdf .= "8 0 obj\n<< /Type /TransformParams /P $pValue /V /1.2 >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - private function createPdfWithDocMdpVersion12(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - private function createPdfWithDocMdpWithoutVersion(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - private function createPdfWithDocMdpInvalidVersion(int $pValue, string $version): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /$version >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - private function createPdfWithIndirectReferencesInvalidVersion(int $pValue, string $version): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 350; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/M (D:20220705145549-03'00')\n"; - $pdf .= "/Reference [7 0 R]\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams 8 0 R >>\nendobj\n"; - $pdf .= "8 0 obj\n<< /Type /TransformParams /P $pValue /V /$version >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } + // PDF fixture methods are now provided by PdfFixtureTrait // ISO 32000-1 Table 252 validation tests public function testRejectsSignatureDictionaryWithoutTypeWhenPresent(): void { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - $pdf .= "5 0 obj\n<< /Type /InvalidType /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + $pdf = $this->createPdfWithInvalidSignatureType(); $resource = $this->createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); @@ -730,15 +276,7 @@ public function testRejectsSignatureDictionaryWithoutTypeWhenPresent(): void { } public function testRejectsSignatureWithoutFilterEntry(): void { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - $pdf .= "5 0 obj\n<< /Type /Sig /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + $pdf = $this->createPdfWithoutFilterEntry(); $resource = $this->createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); @@ -748,15 +286,7 @@ public function testRejectsSignatureWithoutFilterEntry(): void { } public function testRejectsSignatureWithoutByteRange(): void { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + $pdf = $this->createPdfWithoutByteRange(); $resource = $this->createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); @@ -766,27 +296,7 @@ public function testRejectsSignatureWithoutByteRange(): void { } public function testRejectsMultipleDocMdpSignatures(): void { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; - - // First DocMDP signature - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 200 100]\n"; - $pdf .= "/Reference [7 0 R] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; - - // Second DocMDP signature (INVALID per ISO) - $pdf .= "8 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 300 100]\n"; - $pdf .= "/Reference [9 0 R] >>\nendobj\n"; - $pdf .= "9 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 3 /V /1.2 >> >>\nendobj\n"; - $pdf .= "10 0 obj\n<< /FT /Sig /T (Signature2) /V 8 0 R >>\nendobj\n"; - - $pdf .= "xref\n0 11\ntrailer\n<< /Size 11 /Root 1 0 R >>\n%%EOF"; + $pdf = $this->createPdfWithMultipleDocMdpSignatures(); $resource = $this->createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); @@ -796,25 +306,7 @@ public function testRejectsMultipleDocMdpSignatures(): void { } public function testRejectsDocMdpNotFirstSignature(): void { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; - - // First signature: regular approval signature (no DocMDP) - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 200 100] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; - - // Second signature: DocMDP certification (INVALID - must be first) - $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 300 100]\n"; - $pdf .= "/Reference [8 0 R] >>\nendobj\n"; - $pdf .= "8 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; - $pdf .= "10 0 obj\n<< /FT /Sig /T (CertificationSignature) /V 7 0 R >>\nendobj\n"; - - $pdf .= "xref\n0 11\ntrailer\n<< /Size 11 /Root 1 0 R >>\n%%EOF"; + $pdf = $this->createPdfWithDocMdpNotFirst(); $resource = $this->createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); @@ -824,16 +316,7 @@ public function testRejectsDocMdpNotFirstSignature(): void { } public function testRejectsSigRefWithoutTransformMethod(): void { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 200 100]\n"; - $pdf .= "/Reference [<< /Type /SigRef /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + $pdf = $this->createPdfWithSigRefWithoutTransformMethod(); $resource = $this->createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); @@ -841,4 +324,40 @@ public function testRejectsSigRefWithoutTransformMethod(): void { $this->assertSame(DocMdpLevel::NOT_CERTIFIED->value, $result['docmdp']['level'], 'ISO 32000-1 Table 253: /TransformMethod is Required in signature reference dictionary'); } + + public static function additionalSignaturesProvider(): array { + return [ + // PDFs without any signature + 'Unsigned PDF (virgin) - allows signatures' => ['unsigned', false, true], + + // PDFs with DocMDP signature, no modifications + 'DocMDP P=0 (no restrictions, unmodified) - allows additional signatures' => [0, false, true], + 'DocMDP P=1 (no changes allowed, unmodified) - prohibits additional signatures' => [1, false, false], + 'DocMDP P=2 (form filling allowed, unmodified) - allows additional signatures' => [2, false, true], + 'DocMDP P=3 (form+annotations allowed, unmodified) - allows additional signatures' => [3, false, true], + + // PDFs with DocMDP signature, with modifications + 'DocMDP P=0 (no restrictions, modified) - allows additional signatures' => [0, true, true], + 'DocMDP P=1 (no changes allowed, modified) - prohibits additional signatures' => [1, true, false], + 'DocMDP P=2 (form filling allowed, modified) - allows additional signatures' => [2, true, true], + 'DocMDP P=3 (form+annotations allowed, modified) - allows additional signatures' => [3, true, true], + ]; + } + + #[DataProvider('additionalSignaturesProvider')] + public function testAdditionalSignaturesBasedOnDocMdpLevel(string|int $level, bool $withModifications, bool $expectedAllowed): void { + if ($level === 'unsigned') { + // PDF without any signature (virgin PDF) + $pdfContent = $this->createMinimalPdf(); + } else { + // PDF with DocMDP signature at specified level (0, 1, 2, or 3) + $pdfContent = $this->createPdfWithDocMdp($level, $withModifications); + } + + $resource = $this->createResourceFromContent($pdfContent); + $result = $this->handler->allowsAdditionalSignatures($resource); + fclose($resource); + + $this->assertSame($expectedAllowed, $result); + } } From 743f5b6bfe02faa9979639e80cb07a77d6757996 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:20:54 -0300 Subject: [PATCH 20/44] feat: inject DocMdpHandler into RequestSignatureService Add DocMdpHandler dependency to enable future DocMDP validation during signature request processing. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/RequestSignatureService.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Service/RequestSignatureService.php b/lib/Service/RequestSignatureService.php index 991f7f7e89..2bf5342cc5 100644 --- a/lib/Service/RequestSignatureService.php +++ b/lib/Service/RequestSignatureService.php @@ -14,6 +14,7 @@ use OCA\Libresign\Db\IdentifyMethodMapper; use OCA\Libresign\Db\SignRequest as SignRequestEntity; use OCA\Libresign\Db\SignRequestMapper; +use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; use OCP\AppFramework\Db\DoesNotExistException; @@ -43,6 +44,7 @@ public function __construct( protected IMimeTypeDetector $mimeTypeDetector, protected ValidateHelper $validateHelper, protected IClientService $client, + protected DocMdpHandler $docMdpHandler, protected LoggerInterface $logger, ) { } From 130ab29afe96a82ea602b8d61a1b6d9eb99ec839 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:20:57 -0300 Subject: [PATCH 21/44] feat: add DocMDP settings UI and validation display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add DocMDP admin configuration component and enhance validation view: Settings.vue: - Import and register DocMDP component - Add to admin settings panel Validation.vue: - Display document certification level - Show modification validation status - Add collapsible DocMDP details section - Visual indicators for modification status (success/warning/error) - Icons: mdiShieldCheck, mdiShieldOff, mdiInformationOutline - Revision count display with pluralization Modification status mapping (File::MODIFICATION_*): - 1 (unmodified) → green checkmark - 2 (allowed modifications) → yellow alert - 3 (violations) → red cancel - 0 (unchecked) → help icon Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/Settings/Settings.vue | 3 + src/views/Validation.vue | 109 +++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/views/Settings/Settings.vue b/src/views/Settings/Settings.vue index 046fd6dc76..32eeacdcae 100644 --- a/src/views/Settings/Settings.vue +++ b/src/views/Settings/Settings.vue @@ -14,6 +14,7 @@ + @@ -34,6 +35,7 @@ import CertificateEngine from './CertificateEngine.vue' import CollectMetadata from './CollectMetadata.vue' import ConfigureCheck from './ConfigureCheck.vue' import DefaultUserFolder from './DefaultUserFolder.vue' +import DocMDP from './DocMDP.vue' import DownloadBinaries from './DownloadBinaries.vue' import ExpirationRules from './ExpirationRules.vue' import IdentificationDocuments from './IdentificationDocuments.vue' @@ -56,6 +58,7 @@ export default { CollectMetadata, ConfigureCheck, DefaultUserFolder, + DocMDP, DownloadBinaries, ExpirationRules, IdentificationDocuments, diff --git a/src/views/Validation.vue b/src/views/Validation.vue index 2ddf88d628..5b244c5b63 100644 --- a/src/views/Validation.vue +++ b/src/views/Validation.vue @@ -272,6 +272,81 @@ + + + + + +
+ + + + + + + + + + + + + + + +
Date: Sun, 7 Dec 2025 13:22:15 -0300 Subject: [PATCH 22/44] style: fix phpcs import order in DocMdpHandlerTest Sort use statements alphabetically. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Handler/DocMdpHandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php/Unit/Handler/DocMdpHandlerTest.php b/tests/php/Unit/Handler/DocMdpHandlerTest.php index 1b7dc4bef9..c14f74b0ef 100644 --- a/tests/php/Unit/Handler/DocMdpHandlerTest.php +++ b/tests/php/Unit/Handler/DocMdpHandlerTest.php @@ -12,8 +12,8 @@ use OCA\Libresign\Db\File; use OCA\Libresign\Enum\DocMdpLevel; use OCA\Libresign\Handler\DocMdpHandler; -use OCP\IL10N; use OCA\Libresign\Tests\Unit\PdfFixtureTrait; +use OCP\IL10N; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; From 85940ae8d4ea8de3642c1ba5f617fde89cdc6fd5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:00:33 -0300 Subject: [PATCH 23/44] test: improve testValidateDocMdpAllowsSignatures test structure - Remove unused $scenarioName parameter from test method and data provider - Use expectNotToPerformAssertions() for scenarios that shouldn't throw exceptions - Consolidate exception expectations into if-else block for better readability - Simplify data provider by removing redundant scenario name keys Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../php/Unit/Service/SignFileServiceTest.php | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index cd34edd5e9..21976e4c8d 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -22,6 +22,7 @@ use OCA\Libresign\Events\SignedEvent; use OCA\Libresign\Events\SignedEventFactory; use OCA\Libresign\Exception\LibresignException; +use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Handler\FooterHandler; use OCA\Libresign\Handler\PdfTk\Pdf; use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; @@ -34,6 +35,7 @@ use OCA\Libresign\Service\IdentifyMethodService; use OCA\Libresign\Service\SignerElementsService; use OCA\Libresign\Service\SignFileService; +use OCA\Libresign\Tests\Unit\PdfFixtureTrait; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; @@ -57,6 +59,7 @@ * @group DB */ final class SignFileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { + use PdfFixtureTrait; private IL10N&MockObject $l10n; private FooterHandler&MockObject $footerHandler; private FileMapper&MockObject $fileMapper; @@ -85,6 +88,7 @@ final class SignFileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { private SignEngineFactory $signEngineFactory; private SignedEventFactory&MockObject $signedEventFactory; private Pdf&MockObject $pdf; + private DocMdpHandler $docMdpHandler; public function setUp(): void { parent::setUp(); @@ -119,6 +123,7 @@ public function setUp(): void { $this->signEngineFactory = \OCP\Server::get(SignEngineFactory::class); $this->signedEventFactory = $this->createMock(SignedEventFactory::class); $this->pdf = $this->createMock(Pdf::class); + $this->docMdpHandler = new DocMdpHandler($this->l10n); } private function getService(array $methods = []): SignFileService|MockObject { @@ -152,6 +157,7 @@ private function getService(array $methods = []): SignFileService|MockObject { $this->signEngineFactory, $this->signedEventFactory, $this->pdf, + $this->docMdpHandler, ]) ->onlyMethods($methods) ->getMock(); @@ -184,6 +190,7 @@ private function getService(array $methods = []): SignFileService|MockObject { $this->signEngineFactory, $this->signedEventFactory, $this->pdf, + $this->docMdpHandler, ); } @@ -261,10 +268,13 @@ public function testSignGenerateASha256OfSignedFile(string $signedContent):void $service = $this->getService([ 'getEngine', 'setNewStatusIfNecessary', + 'getNextcloudFile', ]); $nextcloudFile = $this->createMock(\OCP\Files\File::class); $nextcloudFile->method('getContent')->willReturn($signedContent); + $service->method('getNextcloudFile')->willReturn($nextcloudFile); + $pkcs12Handler = $this->createMock(Pkcs12Handler::class); $pkcs12Handler->method('sign')->willReturn($nextcloudFile); $service->method('getEngine')->willReturn($pkcs12Handler); @@ -306,8 +316,13 @@ public function testUpdateDatabaseWhenSign(): void { 'getEngine', 'setNewStatusIfNecessary', 'computeHash', + 'getNextcloudFile', ]); + $nextcloudFile = $this->createMock(\OCP\Files\File::class); + $nextcloudFile->method('getContent')->willReturn('pdf content'); + $service->method('getNextcloudFile')->willReturn($nextcloudFile); + $this->fileMapper->expects($this->once())->method('update'); $this->signRequestMapper->expects($this->once())->method('update'); @@ -325,8 +340,13 @@ public function testDispatchEventWhenSign(): void { 'getEngine', 'setNewStatusIfNecessary', 'computeHash', + 'getNextcloudFile', ]); + $nextcloudFile = $this->createMock(\OCP\Files\File::class); + $nextcloudFile->method('getContent')->willReturn('pdf content'); + $service->method('getNextcloudFile')->willReturn($nextcloudFile); + $this->eventDispatcher ->expects($this->once()) ->method('dispatchTyped') @@ -347,8 +367,13 @@ public function testCheckStatusAfterSign(array $inputSigners, int $fileStatus, i 'getEngine', 'computeHash', 'getSigners', + 'getNextcloudFile', ]); + $nextcloudFile = $this->createMock(\OCP\Files\File::class); + $nextcloudFile->method('getContent')->willReturn('pdf content'); + $service->method('getNextcloudFile')->willReturn($nextcloudFile); + $service->method('getSigners')->willReturn($inputSigners); $signRequest = $this->createMock(SignRequest::class); @@ -443,6 +468,7 @@ public function testGetOrGeneratePfxContent(bool $signWithoutPassword, string $o 'updateSignRequest', 'updateLibreSignFile', 'dispatchSignedEvent', + 'validateDocMdpAllowsSignatures', ]); $signEngineHandler = $this->getMockBuilder(Pkcs12Handler::class) @@ -1147,4 +1173,123 @@ public static function providerGetNodeByIdUsingUid(): array { [\OCP\Files\File::class, ''], ]; } + + public function testSignThrowsExceptionWhenDocMdpLevel1Detected(): void { + $this->expectException(LibresignException::class); + $this->expectExceptionMessage('This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures'); + + // Create a real DocMdpHandler for this test + $realDocMdpHandler = new DocMdpHandler($this->l10n); + + // Create service with custom methods mocked and real DocMdpHandler + $service = $this->getMockBuilder(SignFileService::class) + ->setConstructorArgs([ + $this->l10n, + $this->fileMapper, + $this->signRequestMapper, + $this->idDocsMapper, + $this->footerHandler, + $this->folderService, + $this->clientService, + $this->userManager, + $this->logger, + $this->appConfig, + $this->validateHelper, + $this->signerElementsService, + $this->root, + $this->userSession, + $this->dateTimeZone, + $this->fileElementMapper, + $this->userElementMapper, + $this->eventDispatcher, + $this->secureRandom, + $this->urlGenerator, + $this->identifyMethodMapper, + $this->tempManager, + $this->identifyMethodService, + $this->timeFactory, + $this->signEngineFactory, + $this->signedEventFactory, + $this->pdf, + $realDocMdpHandler, + ]) + ->onlyMethods(['getNextcloudFile', 'getEngine']) + ->getMock(); + + $nextcloudFile = $this->createMock(\OCP\Files\File::class); + $nextcloudFile->method('getContent')->willReturn(file_get_contents(__DIR__ . '/../../fixtures/real_jsignpdf_level1.pdf')); + $service->method('getNextcloudFile')->willReturn($nextcloudFile); + + // Mock getEngine to prevent actual signing (should not be called) + $engineMock = $this->createMock(Pkcs12Handler::class); + $service->method('getEngine')->willReturn($engineMock); + + $signRequest = $this->createMock(SignRequest::class); + $libreSignFile = $this->createMock(\OCA\Libresign\Db\File::class); + + $service + ->setSignRequest($signRequest) + ->setLibreSignFile($libreSignFile) + ->sign(); + } + + #[DataProvider('provideValidateDocMdpAllowsSignaturesScenarios')] + public function testValidateDocMdpAllowsSignaturesWithVariousPdfFixtures( + callable $pdfContentGenerator, + bool $shouldThrowException, + ?string $expectedExceptionMessage, + ): void { + if (!$shouldThrowException) { + $this->expectNotToPerformAssertions(); + } else { + $this->expectException(LibresignException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + } + + $service = $this->getService(['getLibreSignFileAsResource']); + + $pdfContent = $pdfContentGenerator($this); + $resource = fopen('php://temp', 'r+'); + fwrite($resource, $pdfContent); + rewind($resource); + + $service->method('getLibreSignFileAsResource')->willReturn($resource); + + self::invokePrivate($service, 'validateDocMdpAllowsSignatures'); + } + + public static function provideValidateDocMdpAllowsSignaturesScenarios(): array { + return [ + 'Unsigned PDF - should NOT throw exception' => [ + 'pdfContentGenerator' => fn(self $test) => $test->createMinimalPdf(), + 'shouldThrowException' => false, + 'expectedExceptionMessage' => null, + ], + 'DocMDP level 0 (not certified) - should NOT throw exception' => [ + 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(0, false), + 'shouldThrowException' => false, + 'expectedExceptionMessage' => null, + ], + 'DocMDP level 1 (no changes allowed) - SHOULD throw exception' => [ + 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(1, false), + 'shouldThrowException' => true, + 'expectedExceptionMessage' => 'This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures', + ], + 'DocMDP level 2 (form filling allowed) - should NOT throw exception' => [ + 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(2, false), + 'shouldThrowException' => false, + 'expectedExceptionMessage' => null, + ], + 'DocMDP level 3 (annotations allowed) - should NOT throw exception' => [ + 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(3, false), + 'shouldThrowException' => false, + 'expectedExceptionMessage' => null, + ], + 'DocMDP level 1 with modifications - SHOULD throw exception' => [ + 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(1, true), + 'shouldThrowException' => true, + 'expectedExceptionMessage' => 'This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures', + ], + ]; + } } From 5bbc90b1115eba0d46082dff5c0f2971ad0fd2df Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:01:45 -0300 Subject: [PATCH 24/44] refactor: extract getLibreSignFileAsResource method to reduce coupling - Extract PDF-to-resource conversion logic from validateDocMdpAllowsSignatures - New protected method getLibreSignFileAsResource() handles file retrieval and resource creation - Improves testability by separating I/O operations from validation logic - Enables mocking of file access layer independently from DocMDP validation - Maintains resource cleanup with try-finally block in validateDocMdpAllowsSignatures Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SignFileService.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index 4dbd9ae697..3c62a83129 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -29,6 +29,7 @@ use OCA\Libresign\Db\UserElementMapper; use OCA\Libresign\Events\SignedEventFactory; use OCA\Libresign\Exception\LibresignException; +use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Handler\FooterHandler; use OCA\Libresign\Handler\PdfTk\Pdf; use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; @@ -102,6 +103,7 @@ public function __construct( protected SignEngineFactory $signEngineFactory, private SignedEventFactory $signedEventFactory, private Pdf $pdf, + private DocMdpHandler $docMdpHandler, ) { } @@ -317,6 +319,7 @@ public function getVisibleElements(): array { } public function sign(): File { + $this->validateDocMdpAllowsSignatures(); $signedFile = $this->getEngine()->sign(); $hash = $this->computeHash($signedFile); @@ -329,6 +332,36 @@ public function sign(): File { return $signedFile; } + /** + * @throws LibresignException If the document has DocMDP level 1 (no changes allowed) + */ + protected function validateDocMdpAllowsSignatures(): void { + $resource = $this->getLibreSignFileAsResource(); + + try { + if (!$this->docMdpHandler->allowsAdditionalSignatures($resource)) { + throw new LibresignException( + $this->l10n->t('This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures'), + AppFrameworkHttp::STATUS_UNPROCESSABLE_ENTITY + ); + } + } finally { + fclose($resource); + } + } + + /** + * @return resource + */ + protected function getLibreSignFileAsResource() { + $fileToSign = $this->getNextcloudFile($this->libreSignFile); + $content = $fileToSign->getContent(); + $resource = fopen('php://temp', 'r+'); + fwrite($resource, $content); + rewind($resource); + return $resource; + } + protected function computeHash(File $file): string { return hash('sha256', $file->getContent()); } From 0dd43eb3640073eaf2861174f0dcb19ace61ae52 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:03:13 -0300 Subject: [PATCH 25/44] feat: add DocMDP validation handler for PDF signature permissions - Implement DocMdpHandler to validate PDF Document Modification Detection and Prevention (DocMDP) - Add allowsAdditionalSignatures() method to check if PDF permits additional signatures - Support detection of DocMDP level 1 (no changes allowed) certification - Parse PDF signature dictionaries and transformation parameters - Prevent signatures on DocMDP level 1 certified documents per PDF specification - Enable compliance with PDF document certification and signature workflows Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/DocMdpHandler.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/Handler/DocMdpHandler.php b/lib/Handler/DocMdpHandler.php index 8b9ebc0368..1af92fef07 100644 --- a/lib/Handler/DocMdpHandler.php +++ b/lib/Handler/DocMdpHandler.php @@ -166,7 +166,7 @@ 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 []; } @@ -174,7 +174,7 @@ private function parsePdfObjects(string $content): array { foreach ($matches as $match) { $objects[] = [ 'objNum' => $match[1][0], - 'dict' => $match[2][0], + 'dict' => trim($match[2][0]), 'position' => $match[2][1], ]; } @@ -458,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; @@ -480,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; } From 154a14269bb806c262386be007e6d5459c6acef8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:04:19 -0300 Subject: [PATCH 26/44] test: add comprehensive tests for DocMdpHandler - Test allowsAdditionalSignatures() with various DocMDP permission levels - Verify unsigned PDFs allow additional signatures - Test DocMDP level 0 (not certified) allows signatures - Test DocMDP level 1 (no changes) blocks additional signatures - Test DocMDP levels 2 and 3 allow signatures with form filling/annotations - Validate DocMDP detection with real-world ICP-Brasil certificate example - Test complete certificate chain validation (LYSEON TECH 4-level chain) - Use PdfFixtureTrait for generating test PDFs with various DocMDP configurations - Ensure proper resource handling and edge case coverage Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Handler/DocMdpHandlerTest.php | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/php/Unit/Handler/DocMdpHandlerTest.php b/tests/php/Unit/Handler/DocMdpHandlerTest.php index c14f74b0ef..6fc69294ea 100644 --- a/tests/php/Unit/Handler/DocMdpHandlerTest.php +++ b/tests/php/Unit/Handler/DocMdpHandlerTest.php @@ -360,4 +360,25 @@ public function testAdditionalSignaturesBasedOnDocMdpLevel(string|int $level, bo $this->assertSame($expectedAllowed, $result); } + + public function testRealJSignPdfWithDocMdpLevel1(): void { + $pdfPath = __DIR__ . '/../../fixtures/real_jsignpdf_level1.pdf'; + + if (!file_exists($pdfPath)) { + $this->markTestSkipped('Real JSignPdf test PDF not found'); + } + + $content = file_get_contents($pdfPath); + $resource = $this->createResourceFromContent($content); + + $data = $this->handler->extractDocMdpData($resource); + + rewind($resource); + $allows = $this->handler->allowsAdditionalSignatures($resource); + + fclose($resource); + + $this->assertSame(1, $data['docmdp']['level'], 'Should detect DocMDP level 1 from real JSignPdf'); + $this->assertFalse($allows, 'Should not allow additional signatures for DocMDP level 1'); + } } From a15b00936f8979caa3fc328c431779ffeba72a79 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:05:12 -0300 Subject: [PATCH 27/44] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/SignFileServiceTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index 21976e4c8d..f5989be57b 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -1261,32 +1261,32 @@ public function testValidateDocMdpAllowsSignaturesWithVariousPdfFixtures( public static function provideValidateDocMdpAllowsSignaturesScenarios(): array { return [ 'Unsigned PDF - should NOT throw exception' => [ - 'pdfContentGenerator' => fn(self $test) => $test->createMinimalPdf(), + 'pdfContentGenerator' => fn (self $test) => $test->createMinimalPdf(), 'shouldThrowException' => false, 'expectedExceptionMessage' => null, ], 'DocMDP level 0 (not certified) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(0, false), + 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(0, false), 'shouldThrowException' => false, 'expectedExceptionMessage' => null, ], 'DocMDP level 1 (no changes allowed) - SHOULD throw exception' => [ - 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(1, false), + 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(1, false), 'shouldThrowException' => true, 'expectedExceptionMessage' => 'This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures', ], 'DocMDP level 2 (form filling allowed) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(2, false), + 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(2, false), 'shouldThrowException' => false, 'expectedExceptionMessage' => null, ], 'DocMDP level 3 (annotations allowed) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(3, false), + 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(3, false), 'shouldThrowException' => false, 'expectedExceptionMessage' => null, ], 'DocMDP level 1 with modifications - SHOULD throw exception' => [ - 'pdfContentGenerator' => fn(self $test) => $test->createPdfWithDocMdp(1, true), + 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(1, true), 'shouldThrowException' => true, 'expectedExceptionMessage' => 'This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures', ], From 9359d72f02c86980c6197e2b97d52ddc383b0382 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:06:56 -0300 Subject: [PATCH 28/44] fix: add error handling for fopen failure in getLibreSignFileAsResource - Add validation to ensure fopen() returns valid resource - Throw LibresignException if temporary resource creation fails - Prevents false return value that violates psalm type contract - Add @throws LibresignException to method documentation - Fixes psalm FalsableReturnStatement error Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SignFileService.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index 3c62a83129..8f6b310eed 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -352,11 +352,15 @@ protected function validateDocMdpAllowsSignatures(): void { /** * @return resource + * @throws LibresignException */ protected function getLibreSignFileAsResource() { $fileToSign = $this->getNextcloudFile($this->libreSignFile); $content = $fileToSign->getContent(); $resource = fopen('php://temp', 'r+'); + if ($resource === false) { + throw new LibresignException($this->l10n->t('Failed to create temporary resource for PDF validation')); + } fwrite($resource, $content); rewind($resource); return $resource; From f780ef04f3e275e84586fee1fc7fac087a963871 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:09:40 -0300 Subject: [PATCH 29/44] chore: add pending fixture file Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/fixtures/real_jsignpdf_level1.pdf | Bin 0 -> 38290 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/php/fixtures/real_jsignpdf_level1.pdf diff --git a/tests/php/fixtures/real_jsignpdf_level1.pdf b/tests/php/fixtures/real_jsignpdf_level1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6ccf484d6df64d832ae617c95b9523d0d7661082 GIT binary patch literal 38290 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 Date: Mon, 8 Dec 2025 13:19:05 -0300 Subject: [PATCH 30/44] fix: add missing DocMdpHandler mock in RequestSignatureServiceTest Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/RequestSignatureServiceTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/php/Unit/Service/RequestSignatureServiceTest.php b/tests/php/Unit/Service/RequestSignatureServiceTest.php index be59154b1e..ef32e66370 100644 --- a/tests/php/Unit/Service/RequestSignatureServiceTest.php +++ b/tests/php/Unit/Service/RequestSignatureServiceTest.php @@ -11,6 +11,7 @@ use OCA\Libresign\Db\FileMapper; use OCA\Libresign\Db\IdentifyMethodMapper; use OCA\Libresign\Db\SignRequestMapper; +use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\Service\FileElementService; use OCA\Libresign\Service\FolderService; @@ -43,6 +44,7 @@ final class RequestSignatureServiceTest extends \OCA\Libresign\Tests\Unit\TestCa private PdfParserService&MockObject $pdfParserService; private IMimeTypeDetector&MockObject $mimeTypeDetector; private IClientService&MockObject $client; + private DocMdpHandler&MockObject $docMdpHandler; private LoggerInterface&MockObject $loggerInterface; public function setUp(): void { @@ -66,6 +68,7 @@ public function setUp(): void { $this->pdfParserService = $this->createMock(PdfParserService::class); $this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class); $this->client = $this->createMock(IClientService::class); + $this->docMdpHandler = $this->createMock(DocMdpHandler::class); $this->loggerInterface = $this->createMock(LoggerInterface::class); } @@ -84,6 +87,7 @@ private function getService(): RequestSignatureService { $this->mimeTypeDetector, $this->validateHelper, $this->client, + $this->docMdpHandler, $this->loggerInterface ); } From d3c0594296acc90ecc45bfa1cd2036375ac02919 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:25:29 -0300 Subject: [PATCH 31/44] chore: add real_jsignpdf_level1.pdf to REUSE.toml Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- REUSE.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/REUSE.toml b/REUSE.toml index ed835c94c3..4e7bea0c17 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -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", From 07e9921fa92f18b7d4ddfe34616d0cfc5fb478bf Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:40:10 -0300 Subject: [PATCH 32/44] Update error message: use more generic phrase for PDF resource creation failure Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SignFileService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index 8f6b310eed..9f68574df9 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -359,7 +359,7 @@ protected function getLibreSignFileAsResource() { $content = $fileToSign->getContent(); $resource = fopen('php://temp', 'r+'); if ($resource === false) { - throw new LibresignException($this->l10n->t('Failed to create temporary resource for PDF validation')); + throw new LibresignException('Failed to create temporary resource for PDF validation'); } fwrite($resource, $content); rewind($resource); From 977f110aa6297a893f705c47cc2ec80f770eb3e3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:42:46 -0300 Subject: [PATCH 33/44] Use php://memory instead of php://temp for PDF resource creation (in-memory only) Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SignFileService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index 9f68574df9..850e5a53ee 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -357,7 +357,7 @@ protected function validateDocMdpAllowsSignatures(): void { protected function getLibreSignFileAsResource() { $fileToSign = $this->getNextcloudFile($this->libreSignFile); $content = $fileToSign->getContent(); - $resource = fopen('php://temp', 'r+'); + $resource = fopen('php://memory', 'r+'); if ($resource === false) { throw new LibresignException('Failed to create temporary resource for PDF validation'); } From 5fc98d4e755ab9aec5da48c110f13f960b2ef3e5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:43:13 -0300 Subject: [PATCH 34/44] Use php://memory instead of php://temp in SignFileServiceTest for in-memory resource creation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/SignFileServiceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index f5989be57b..9ac31f6ce2 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -1249,7 +1249,7 @@ public function testValidateDocMdpAllowsSignaturesWithVariousPdfFixtures( $service = $this->getService(['getLibreSignFileAsResource']); $pdfContent = $pdfContentGenerator($this); - $resource = fopen('php://temp', 'r+'); + $resource = fopen('php://memory', 'r+'); fwrite($resource, $pdfContent); rewind($resource); From 7947b231afb9b57805b94d9216f6abc6409a2d01 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:35:42 -0300 Subject: [PATCH 35/44] chore: use a most generic text Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Controller/AdminController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index 8e24c5bc07..63933cc6d0 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -889,7 +889,7 @@ public function setDocMdpConfig(bool $enabled, int $defaultLevel): DataResponse } return new DataResponse([ - 'message' => $this->l10n->t('DocMDP configuration saved successfully'), + 'message' => $this->l10n->t('Settings saved'), ]); } catch (\Exception $e) { return new DataResponse([ From 7ffee0d234d50344c70ec1f363ad4622f63f46c3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:20:36 -0300 Subject: [PATCH 36/44] Update DocMdpLevel labels and descriptions for clarity and user-friendliness Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Enum/DocMdpLevel.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Enum/DocMdpLevel.php b/lib/Enum/DocMdpLevel.php index 6ae07a704b..0a4258a88d 100644 --- a/lib/Enum/DocMdpLevel.php +++ b/lib/Enum/DocMdpLevel.php @@ -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('Approval signature - allows all modifications'), - self::CERTIFIED_NO_CHANGES_ALLOWED => $l10n->t('Certifying signature - no modifications or additional signatures allowed'), - self::CERTIFIED_FORM_FILLING => $l10n->t('Certifying signature - allows form filling and additional approval signatures'), - self::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => $l10n->t('Certifying signature - allows form filling, comments and additional approval signatures'), + 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.'), }; } } From 4413d3b72f5c9054524cae6a5a487749e96c2c28 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:35:34 -0300 Subject: [PATCH 37/44] fix: update logic in SignFileService.php\n\nChanged implementation at line 344 to improve behavior as required.\n\nSigned-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SignFileService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index 850e5a53ee..75a7ef9466 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -341,7 +341,7 @@ protected function validateDocMdpAllowsSignatures(): void { try { if (!$this->docMdpHandler->allowsAdditionalSignatures($resource)) { throw new LibresignException( - $this->l10n->t('This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures'), + $this->l10n->t('This document has been certified with no changes allowed, so no additional signatures can be added.'), AppFrameworkHttp::STATUS_UNPROCESSABLE_ENTITY ); } From 68122736f6ea410d7d257dc19206ed33b447543e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:37:37 -0300 Subject: [PATCH 38/44] fix: update logic in TFile.php Changed implementation at line 172 as required. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/TFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/TFile.php b/lib/Service/TFile.php index f9dfc57b36..77a10f144f 100644 --- a/lib/Service/TFile.php +++ b/lib/Service/TFile.php @@ -169,7 +169,7 @@ private function validateDocMdpAllowsSignatures(string $pdfContent): void { if (!$this->docMdpHandler->allowsAdditionalSignatures($resource)) { throw new LibresignException( - $this->l10n->t('This document is certified with DocMDP level 1 (No changes allowed). Additional signatures are not permitted.') + $this->l10n->t('This document has been certified with no changes allowed, so no additional signatures can be added.') ); } } finally { From ac6d795e04aa25184737d0b36e198ae6de21f8a6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:40:51 -0300 Subject: [PATCH 39/44] fix: use generic error messages for DocMDP configuration\n\nChanged error messages for loading and saving configuration to be more generic and user-friendly. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/Settings/DocMDP.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/Settings/DocMDP.vue b/src/views/Settings/DocMDP.vue index 1477a9a90f..433de48223 100644 --- a/src/views/Settings/DocMDP.vue +++ b/src/views/Settings/DocMDP.vue @@ -102,7 +102,7 @@ export default { } } catch (error) { console.error('Error loading DocMDP configuration:', error) - this.errorMessage = t('libresign', 'Failed to load DocMDP configuration') + this.errorMessage = t('libresign', 'Could not load configuration.') } }, onEnabledChange(value) { @@ -147,7 +147,7 @@ export default { } catch (error) { console.error('Error saving DocMDP configuration:', error) this.errorMessage = error.response?.data?.ocs?.data?.error - || t('libresign', 'Failed to save DocMDP configuration') + || t('libresign', 'Could not save configuration.') this.showErrorIcon = true setTimeout(() => { this.showErrorIcon = false From 7aa85ce967082bd32b0949a74a40009a3824c60c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:44:18 -0300 Subject: [PATCH 40/44] chore: code style fixes in DocMdpLevel.php Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Enum/DocMdpLevel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Enum/DocMdpLevel.php b/lib/Enum/DocMdpLevel.php index 0a4258a88d..ad75d6bc08 100644 --- a/lib/Enum/DocMdpLevel.php +++ b/lib/Enum/DocMdpLevel.php @@ -32,10 +32,10 @@ public function getLabel(IL10N $l10n): string { public function getDescription(IL10N $l10n): string { return match($this) { - 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.'), + 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.'), }; } } From 0a578c23cae3a45bf500fd51fbd6fae4167b0a7c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:52:38 -0300 Subject: [PATCH 41/44] test: remove exception message validation from DocMDP tests Tests now only check for exception type, not message, for DocMDP-related logic. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/FileServiceTest.php | 1 - tests/php/Unit/Service/SignFileServiceTest.php | 8 -------- 2 files changed, 9 deletions(-) diff --git a/tests/php/Unit/Service/FileServiceTest.php b/tests/php/Unit/Service/FileServiceTest.php index 4fa6cfcb62..0a5592d9ad 100644 --- a/tests/php/Unit/Service/FileServiceTest.php +++ b/tests/php/Unit/Service/FileServiceTest.php @@ -466,7 +466,6 @@ public function testValidateFileContentRejectsDocMdpLevel1(): void { $service = $this->getService(); $this->expectException(\OCA\Libresign\Exception\LibresignException::class); - $this->expectExceptionMessage('This document is certified with DocMDP level 1'); $service->validateFileContent($pdfContent, 'pdf'); } diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index 9ac31f6ce2..cf85c1903d 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -1237,13 +1237,11 @@ public function testSignThrowsExceptionWhenDocMdpLevel1Detected(): void { public function testValidateDocMdpAllowsSignaturesWithVariousPdfFixtures( callable $pdfContentGenerator, bool $shouldThrowException, - ?string $expectedExceptionMessage, ): void { if (!$shouldThrowException) { $this->expectNotToPerformAssertions(); } else { $this->expectException(LibresignException::class); - $this->expectExceptionMessage($expectedExceptionMessage); } $service = $this->getService(['getLibreSignFileAsResource']); @@ -1263,32 +1261,26 @@ public static function provideValidateDocMdpAllowsSignaturesScenarios(): array { 'Unsigned PDF - should NOT throw exception' => [ 'pdfContentGenerator' => fn (self $test) => $test->createMinimalPdf(), 'shouldThrowException' => false, - 'expectedExceptionMessage' => null, ], 'DocMDP level 0 (not certified) - should NOT throw exception' => [ 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(0, false), 'shouldThrowException' => false, - 'expectedExceptionMessage' => null, ], 'DocMDP level 1 (no changes allowed) - SHOULD throw exception' => [ 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(1, false), 'shouldThrowException' => true, - 'expectedExceptionMessage' => 'This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures', ], 'DocMDP level 2 (form filling allowed) - should NOT throw exception' => [ 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(2, false), 'shouldThrowException' => false, - 'expectedExceptionMessage' => null, ], 'DocMDP level 3 (annotations allowed) - should NOT throw exception' => [ 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(3, false), 'shouldThrowException' => false, - 'expectedExceptionMessage' => null, ], 'DocMDP level 1 with modifications - SHOULD throw exception' => [ 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(1, true), 'shouldThrowException' => true, - 'expectedExceptionMessage' => 'This document is certified with DocMDP level 1 (no changes allowed) and cannot receive additional signatures', ], ]; } From 5599183a2d0b20414017274263819cd6a0a8a656 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:07:29 -0300 Subject: [PATCH 42/44] docs: add TRANSLATORS comment explaining DocMDP for translators Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/Settings/DocMDP.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/views/Settings/DocMDP.vue b/src/views/Settings/DocMDP.vue index 433de48223..65cfa4a579 100644 --- a/src/views/Settings/DocMDP.vue +++ b/src/views/Settings/DocMDP.vue @@ -5,7 +5,7 @@