diff --git a/modules/security/i18n/default_de.json b/modules/security/i18n/default_de.json index 47c691b..d4f1a35 100644 --- a/modules/security/i18n/default_de.json +++ b/modules/security/i18n/default_de.json @@ -75,6 +75,8 @@ "Customer name": "Kundenname", "Customer number": "Kundennummer", "Responsible person": "Verantwortliche Person", + "Official customer report": "Offizieller Kundenbericht", + "Official customer report text (written by the owner on the report step)": "Offizieller Kundenbericht-Text (vom Verantwortlichen im Berichtsschritt verfasst)", "Completion percentage": "Fortschritt in Prozent", "Completed technical checks": "Abgeschlossene technische Prüfungen", "Total technical checks": "Technische Prüfungen gesamt", diff --git a/modules/security/i18n/default_en.json b/modules/security/i18n/default_en.json index 07c1844..672f4a4 100644 --- a/modules/security/i18n/default_en.json +++ b/modules/security/i18n/default_en.json @@ -75,6 +75,8 @@ "Customer name": "Customer name", "Customer number": "Customer number", "Responsible person": "Responsible person", + "Official customer report": "Official customer report", + "Official customer report text (written by the owner on the report step)": "Official customer report text (written by the owner on the report step)", "Completion percentage": "Completion percentage", "Completed technical checks": "Completed technical checks", "Total technical checks": "Total technical checks", diff --git a/modules/security/lib/Module/Security/Service/SecurityCheckProcess.php b/modules/security/lib/Module/Security/Service/SecurityCheckProcess.php index 89b4485..daa3e2c 100644 --- a/modules/security/lib/Module/Security/Service/SecurityCheckProcess.php +++ b/modules/security/lib/Module/Security/Service/SecurityCheckProcess.php @@ -32,10 +32,13 @@ final class SecurityCheckProcess public const FIELD_TEXTAREA = 'textarea'; public const FIELD_DATETIME = 'datetime'; + /** Field key of the official, customer-facing report written on the final step. */ + public const FIELD_CUSTOMER_REPORT = 'customer_report'; + /** * Ordered fixed process steps. * - * @return list}> + * @return list}> */ public function steps(): array { @@ -86,10 +89,13 @@ final class SecurityCheckProcess 'kind' => self::KIND_TECH_CHECKLIST, ], [ - 'key' => 'report', + 'key' => self::STEP_REPORT, 'label' => 'Create report & notify customer', 'description' => 'Create the report for the customer and notify them.', 'kind' => self::KIND_STEP, + 'fields' => [ + ['key' => self::FIELD_CUSTOMER_REPORT, 'label' => 'Official customer report', 'type' => self::FIELD_TEXTAREA, 'required' => true, 'rows' => 12], + ], ], ]; } @@ -114,7 +120,7 @@ final class SecurityCheckProcess /** * Structured fields captured on a given step (info textareas, datetimes, …). * - * @return list + * @return list */ public function stepFields(string $stepKey): array { diff --git a/modules/security/lib/Module/Security/Service/SecurityReportPdfService.php b/modules/security/lib/Module/Security/Service/SecurityReportPdfService.php index f9fd943..2bf87cd 100644 --- a/modules/security/lib/Module/Security/Service/SecurityReportPdfService.php +++ b/modules/security/lib/Module/Security/Service/SecurityReportPdfService.php @@ -106,8 +106,14 @@ final class SecurityReportPdfService $progress = is_array($check['progress'] ?? null) ? $check['progress'] : []; $status = (string) ($check['status'] ?? SecurityCheckProcess::STATUS_OPEN); + // The owner-authored, customer-facing report text captured on the final step. + $reportFields = is_array($processState[SecurityCheckProcess::STEP_REPORT]['fields'] ?? null) + ? $processState[SecurityCheckProcess::STEP_REPORT]['fields'] + : []; + return [ 'title' => t('Security check report'), + 'customer_report' => trim((string) ($reportFields[SecurityCheckProcess::FIELD_CUSTOMER_REPORT] ?? '')), 'customer_name' => (string) ($check['debitor_name'] ?? ''), 'customer_no' => (string) ($check['debitor_no'] ?? ''), 'domain' => (string) ($check['domain_url'] ?? ($check['domain_no'] ?? '')), diff --git a/modules/security/lib/Module/Security/Service/SecurityReportTemplateService.php b/modules/security/lib/Module/Security/Service/SecurityReportTemplateService.php index 3427a9b..9bd044b 100644 --- a/modules/security/lib/Module/Security/Service/SecurityReportTemplateService.php +++ b/modules/security/lib/Module/Security/Service/SecurityReportTemplateService.php @@ -43,6 +43,7 @@ final class SecurityReportTemplateService public function availableVariables(): array { return [ + 'customer_report' => t('Official customer report text (written by the owner on the report step)'), 'logo_src' => t('Logo image source (data URI) — use in '), 'title' => t('Report title'), 'customer_name' => t('Customer name'), @@ -93,6 +94,9 @@ final class SecurityReportTemplateService $map['{{' . $key . '}}'] = $this->esc($value); } + // Owner-authored prose: escape, then keep the author's line breaks. + $map['{{customer_report}}'] = nl2br($this->esc((string) ($context['customer_report'] ?? ''))); + $map['{{checklist}}'] = $this->buildChecklistHtml( is_array($context['tech_sections'] ?? null) ? $context['tech_sections'] : [] ); @@ -214,6 +218,9 @@ final class SecurityReportTemplateService
{{checks_done}} / {{checks_total}} ({{percent}}%)
+

Report

+
{{customer_report}}
+

Technical checklist

{{checklist}} diff --git a/modules/security/pages/security/checks/edit(default).phtml b/modules/security/pages/security/checks/edit(default).phtml index f3ab442..618a992 100644 --- a/modules/security/pages/security/checks/edit(default).phtml +++ b/modules/security/pages/security/checks/edit(default).phtml @@ -186,7 +186,7 @@ $completionMeta = static function (array $entry) use ($userNames): string { data-security-required="1" disabled> - + diff --git a/modules/security/tests/Module/Security/Service/SecurityCheckProcessTest.php b/modules/security/tests/Module/Security/Service/SecurityCheckProcessTest.php index ea47c52..86f816f 100644 --- a/modules/security/tests/Module/Security/Service/SecurityCheckProcessTest.php +++ b/modules/security/tests/Module/Security/Service/SecurityCheckProcessTest.php @@ -36,6 +36,23 @@ class SecurityCheckProcessTest extends TestCase $this->assertContains('external_systems', $keys); } + public function testReportStepRequiresOfficialCustomerReportField(): void + { + $process = new SecurityCheckProcess(); + + $fields = $process->stepFields(SecurityCheckProcess::STEP_REPORT); + $this->assertSame( + [SecurityCheckProcess::FIELD_CUSTOMER_REPORT], + array_column($fields, 'key') + ); + + // The owner must fill it before the final step can be completed. + $this->assertContains( + SecurityCheckProcess::FIELD_CUSTOMER_REPORT, + $process->requiredFieldKeys(SecurityCheckProcess::STEP_REPORT) + ); + } + public function testRequiredFieldKeysExcludeOptionalFields(): void { $keys = (new SecurityCheckProcess())->requiredFieldKeys(SecurityCheckProcess::STEP_INFORMATION); diff --git a/modules/security/tests/Module/Security/Service/SecurityCheckServiceTest.php b/modules/security/tests/Module/Security/Service/SecurityCheckServiceTest.php index 81ad850..bd4d6c9 100644 --- a/modules/security/tests/Module/Security/Service/SecurityCheckServiceTest.php +++ b/modules/security/tests/Module/Security/Service/SecurityCheckServiceTest.php @@ -128,6 +128,9 @@ class SecurityCheckServiceTest extends TestCase 'blocker_start' => '2026-07-01T08:00', 'blocker_end' => '2026-07-02T18:00', ]; + $step[SecurityCheckProcess::STEP_REPORT]['fields'] = [ + SecurityCheckProcess::FIELD_CUSTOMER_REPORT => 'All checks passed; no action required.', + ]; $input = [ 'step' => $step, 'tech' => ['tls' => ['done' => '1', 'note' => 'verified']], @@ -143,6 +146,10 @@ class SecurityCheckServiceTest extends TestCase $this->assertSame(77, $process_state['beauftragung']['by']); $this->assertNotEmpty($process_state['beauftragung']['at']); $this->assertSame('Jane', $process_state[SecurityCheckProcess::STEP_INFORMATION]['fields']['technical_contact']); + $this->assertSame( + 'All checks passed; no action required.', + $process_state[SecurityCheckProcess::STEP_REPORT]['fields'][SecurityCheckProcess::FIELD_CUSTOMER_REPORT] + ); $tech_state = json_decode((string) $captured['tech_state_json'], true); $this->assertTrue($tech_state['tls']['done']); diff --git a/modules/security/tests/Module/Security/Service/SecurityReportPdfServiceTest.php b/modules/security/tests/Module/Security/Service/SecurityReportPdfServiceTest.php index a308445..1e1a4b4 100644 --- a/modules/security/tests/Module/Security/Service/SecurityReportPdfServiceTest.php +++ b/modules/security/tests/Module/Security/Service/SecurityReportPdfServiceTest.php @@ -30,6 +30,9 @@ class SecurityReportPdfServiceTest extends TestCase foreach ($process->manualStepKeys() as $key) { $processState[$key] = ['done' => true, 'at' => '2026-06-18 10:00:00']; } + $processState[SecurityCheckProcess::STEP_REPORT]['fields'] = [ + SecurityCheckProcess::FIELD_CUSTOMER_REPORT => 'No critical findings.', + ]; $snapshot = ['version' => 1, 'items' => [ ['type' => 'section', 'label' => 'Transport'], ['type' => 'check', 'key' => 'tls', 'label' => 'TLS enforced'], @@ -68,6 +71,7 @@ class SecurityReportPdfServiceTest extends TestCase $this->assertSame('acme.de', $context['domain']); $this->assertSame('Intranet', $context['product']); $this->assertSame('Jane Doe', $context['owner']); + $this->assertSame('No critical findings.', $context['customer_report']); $this->assertNotSame('', $context['title']); // Summary mirrors the progress math (3 tech items, 2 done). @@ -99,6 +103,7 @@ class SecurityReportPdfServiceTest extends TestCase $this->assertSame('', $context['customer_name']); $this->assertSame('', $context['domain']); + $this->assertSame('', $context['customer_report']); $this->assertSame([], $context['tech_sections']); $this->assertSame(0, $context['summary']['total']); $this->assertSame(0, $context['summary']['percent']); diff --git a/modules/security/tests/Module/Security/Service/SecurityReportTemplateServiceTest.php b/modules/security/tests/Module/Security/Service/SecurityReportTemplateServiceTest.php index fe46c60..6983619 100644 --- a/modules/security/tests/Module/Security/Service/SecurityReportTemplateServiceTest.php +++ b/modules/security/tests/Module/Security/Service/SecurityReportTemplateServiceTest.php @@ -22,6 +22,7 @@ class SecurityReportTemplateServiceTest extends TestCase 'status_label' => 'In progress', 'generated_at' => '2026-06-22 10:00', 'app_name' => 'CoreCore', + 'customer_report' => "Line one\nLine two", 'logo_data_uri' => 'data:image/png;base64,AAAA', 'summary' => ['percent' => 72, 'tech_done' => 2, 'tech_total' => 3], 'tech_sections' => [ @@ -55,6 +56,22 @@ class SecurityReportTemplateServiceTest extends TestCase $this->assertStringNotContainsString('Acme', $out); } + public function testRenderCustomerReportEscapesAndPreservesLineBreaks(): void + { + $service = new SecurityReportTemplateService(); + + $context = $this->context(); + $context['customer_report'] = "First line