Sanitize survey frontend and PDF output

This commit is contained in:
Jürgen Mummert
2026-05-24 17:53:53 +02:00
parent 64fc578d4c
commit 6eec9ec537
6 changed files with 84 additions and 41 deletions
@@ -2,7 +2,10 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<title>{{ documentTitle|striptags|trim }}</title>
<style>
{{ fontFaceCss|raw }}
@page {
margin: 20mm 14mm 18mm;
}
@@ -10,7 +13,7 @@
body {
margin: 0;
color: #222222;
font-family: DejaVu Sans, sans-serif;
font-family: "PT Sans Narrow", sans-serif;
font-size: 11px;
line-height: 1.45;
background: #ffffff;
@@ -33,6 +36,7 @@
.page-header h1 {
font-size: 22px;
font-family: "Blogger Sans", sans-serif;
color: #0f4f88;
margin-bottom: 8px;
}
@@ -64,6 +68,7 @@
.question-card h2 {
font-size: 16px;
font-family: "Blogger Sans", sans-serif;
color: #0f4f88;
margin-bottom: 6px;
}
@@ -174,6 +179,7 @@
.metric-value {
font-size: 18px;
font-weight: bold;
font-family: "Blogger Sans", sans-serif;
color: #0f4f88;
}
@@ -196,8 +202,8 @@
</head>
<body>
<header class="page-header">
<h1>{{ survey.title }}</h1>
<p>{{ survey.description ?: 'Keine Beschreibung hinterlegt.' }}</p>
<h1>{{ survey.title|striptags|trim }}</h1>
<p>{{ (survey.description|striptags|trim) ?: 'Keine Beschreibung hinterlegt.' }}</p>
<div class="meta-row">
<span class="meta-pill">{{ completedSubmissionCount }} abgeschlossene Teilnahmen</span>
@@ -208,10 +214,10 @@
{% for question in questionResults %}
<section class="question-card">
<h2>{{ question.question }}</h2>
<h2>{{ question.question|striptags|trim }}</h2>
{% if question.description %}
<p class="question-description">{{ question.description }}</p>
{% if question.description|striptags|trim %}
<p class="question-description">{{ question.description|striptags|trim }}</p>
{% endif %}
<div class="meta-row">
@@ -221,7 +227,7 @@
{% if question.pdfChartDataUri %}
<div class="chart-box chart-{{ question.pdfChartType ?: 'default' }}">
<img src="{{ question.pdfChartDataUri }}" alt="Diagramm zu {{ question.question }}">
<img src="{{ question.pdfChartDataUri }}" alt="Diagramm zu {{ question.question|striptags|trim }}">
{% if question.type == 'range' and question.pdfChartLegend %}
<div class="chart-hint">Die Punkte sind farblich den Werten in der Tabelle zugeordnet.</div>
{% endif %}
@@ -242,7 +248,7 @@
{% for option in question.pdfChartLegend %}
<tr>
<td><span class="swatch" style="background: {{ option.fillColor }}; border-color: {{ option.borderColor }};"></span></td>
<td>{{ option.label }}</td>
<td>{{ option.label|striptags|trim }}</td>
<td class="numeric">{{ option.count }}</td>
<td class="numeric">{{ option.percentage }}%</td>
</tr>
@@ -282,7 +288,7 @@
{% for option in question.pdfChartLegend %}
<tr>
<td><span class="swatch" style="background: {{ option.fillColor }}; border-color: {{ option.borderColor }};"></span></td>
<td>{{ option.label }}</td>
<td>{{ option.label|striptags|trim }}</td>
<td class="numeric">{{ option.count }}</td>
<td class="numeric">{{ option.percentage }}%</td>
</tr>
@@ -308,7 +314,7 @@
{% for option in question.pdfChartLegend %}
<tr>
<td><span class="swatch" style="background: {{ option.fillColor }}; border-color: {{ option.borderColor }};"></span></td>
<td>{{ option.label }}</td>
<td>{{ option.label|striptags|trim }}</td>
<td class="numeric">{{ option.count }}</td>
<td class="numeric">{{ option.percentage }}%</td>
</tr>
@@ -319,7 +325,7 @@
<ol class="response-list">
{% for response in question.responses %}
<li>{{ response }}</li>
<li>{{ response|striptags|trim }}</li>
{% else %}
<li>Noch keine Freitextantworten vorhanden.</li>
{% endfor %}
+37
View File
@@ -197,9 +197,46 @@ final class SurveyResultsPdfService
'completedSubmissionCount' => $completedSubmissionCount,
'questionResults' => $questionResults,
'exportedAt' => (new \DateTimeImmutable())->format('d.m.Y H:i'),
'documentTitle' => $this->buildDocumentTitle((string) $survey->title),
'fontFaceCss' => $this->buildEmbeddedFontFaceCss(),
]);
}
private function buildDocumentTitle(string $surveyTitle): string
{
$title = trim(strip_tags($surveyTitle));
return '' !== $title ? $title.' Ergebnisse' : 'Umfrage Ergebnisse';
}
private function buildEmbeddedFontFaceCss(): string
{
$fontDefinitions = [
['family' => 'PT Sans Narrow', 'weight' => 400, 'path' => __DIR__.'/../../public/fonts/PTSansNarrow-Regular.woff2'],
['family' => 'PT Sans Narrow', 'weight' => 700, 'path' => __DIR__.'/../../public/fonts/PTSansNarrow-Bold.woff2'],
['family' => 'Blogger Sans', 'weight' => 500, 'path' => __DIR__.'/../../public/fonts/Blogger Sans-Medium.woff2'],
];
$rules = [];
foreach ($fontDefinitions as $fontDefinition) {
$fontContent = @file_get_contents($fontDefinition['path']);
if (false === $fontContent) {
continue;
}
$rules[] = sprintf(
'@font-face { font-family: "%s"; src: url("data:font/woff2;base64,%s") format("woff2"); font-weight: %d; font-style: normal; font-display: swap; }',
$fontDefinition['family'],
base64_encode($fontContent),
$fontDefinition['weight'],
);
}
return implode("\n", $rules);
}
/**
* @param array<string, mixed> $questionResult
*/