$context Output of SecurityReportPdfService::buildReportContext() * plus app_name / logo_data_uri / generated_at. */ public function render(string $template, array $context): string { return strtr($template, $this->buildVariableMap($context)); } /** * Placeholder => already-translated description, for the UI variable reference * and to document the contract in one place. Keys are the bare token names * (without braces); render() wraps them in {{ }}. * * @return array */ public function availableVariables(): array { return [ 'logo_src' => t('Logo image source (data URI) — use in '), 'title' => t('Report title'), 'customer_name' => t('Customer name'), 'customer_no' => t('Customer number'), 'domain' => t('Domain'), 'product' => t('Software product'), 'owner' => t('Responsible person'), 'status' => t('Status'), 'percent' => t('Completion percentage'), 'checks_done' => t('Completed technical checks'), 'checks_total' => t('Total technical checks'), 'generated_at' => t('Generation date'), 'app_name' => t('Application name'), 'checklist' => t('Technical checklist table (HTML)'), 'process' => t('Process steps table (HTML)'), ]; } /** * Build the {{token}} => value map. Scalars are HTML-escaped; structural * variables are trusted HTML built from the (already escaped) context. * * @param array $context * @return array */ private function buildVariableMap(array $context): array { $summary = is_array($context['summary'] ?? null) ? $context['summary'] : []; $scalars = [ 'logo_src' => (string) ($context['logo_data_uri'] ?? ''), 'title' => (string) ($context['title'] ?? ''), 'customer_name' => (string) ($context['customer_name'] ?? ''), 'customer_no' => (string) ($context['customer_no'] ?? ''), 'domain' => (string) ($context['domain'] ?? ''), 'product' => (string) ($context['product'] ?? ''), 'owner' => (string) ($context['owner'] ?? ''), 'status' => (string) ($context['status_label'] ?? ''), 'percent' => (string) (int) ($summary['percent'] ?? 0), 'checks_done' => (string) (int) ($summary['tech_done'] ?? 0), 'checks_total' => (string) (int) ($summary['tech_total'] ?? 0), 'generated_at' => (string) ($context['generated_at'] ?? ''), 'app_name' => (string) ($context['app_name'] ?? ''), ]; $map = []; foreach ($scalars as $key => $value) { $map['{{' . $key . '}}'] = $this->esc($value); } $map['{{checklist}}'] = $this->buildChecklistHtml( is_array($context['tech_sections'] ?? null) ? $context['tech_sections'] : [] ); $map['{{process}}'] = $this->buildProcessHtml( is_array($context['steps'] ?? null) ? $context['steps'] : [] ); return $map; } /** * @param array $sections */ private function buildChecklistHtml(array $sections): string { if ($sections === []) { return '

' . $this->esc(t('This product has no technical checklist items defined yet.')) . '

'; } $rows = ''; foreach ($sections as $section) { if (!is_array($section)) { continue; } $label = trim((string) ($section['label'] ?? '')); if ($label !== '') { $rows .= '' . $this->esc($label) . ''; } foreach ((is_array($section['items'] ?? null) ? $section['items'] : []) as $item) { if (!is_array($item)) { continue; } $state = !empty($item['done']) ? '✓ ' . $this->esc(t('Completed')) . '' : '' . $this->esc(t('Not completed')) . ''; $note = trim((string) ($item['note'] ?? '')); $noteHtml = $note !== '' ? '
' . $this->esc($note) . '
' : ''; $rows .= '' . $state . '' . $this->esc((string) ($item['label'] ?? '')) . $noteHtml . ''; } } return '' . $rows . '
'; } /** * @param array $steps */ private function buildProcessHtml(array $steps): string { $rows = ''; foreach ($steps as $step) { if (!is_array($step)) { continue; } $state = !empty($step['done']) ? '✓ ' . $this->esc(t('Completed')) . '' : '' . $this->esc(t('Open')) . ''; $completedAt = trim((string) ($step['completed_at'] ?? '')); $when = $completedAt !== '' ? ' · ' . $this->esc($completedAt) . '' : ''; $rows .= '' . $state . '' . $this->esc((string) ($step['number'] ?? '')) . '. ' . $this->esc((string) ($step['label'] ?? '')) . $when . ''; } return '' . $rows . '
'; } /** * A ready-to-edit starter template offered in the editor. Faithful to the * built-in default layout so admins have a working baseline to customise. */ public function defaultTemplateHtml(): string { return <<<'HTML'

{{title}}

{{generated_at}}
Customer{{customer_name}} ({{customer_no}})
Domain{{domain}}
Software product{{product}}
Responsible{{owner}}
Status{{status}}

Result

{{checks_done}} / {{checks_total}} ({{percent}}%)

Technical checklist

{{checklist}}

Process

{{process}} HTML; } private function esc(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } }