From e41679b3314a068916d2b562e41b56b66fab1061 Mon Sep 17 00:00:00 2001 From: Knacky Date: Tue, 9 Jun 2026 18:13:46 +0200 Subject: [PATCH] fix(export): render PDF in A4 landscape for 7-column readability Add @page { size: A4 landscape } to _CSS, reduce font-size to 11px, and set table-layout: fixed + word-break: break-word so 7 columns fit without overflow. Unit test asserts the landscape rule is present in the rendered HTML. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + backend/app/services/export.py | 11 ++++++----- backend/tests/test_export_render.py | 9 +++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b7094..0f9dece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - 2026-06-08 (post-refactor, pre-merge) — **Two MEDIUM security regressions fixed** in the 7-column refactor (`3a9d9d3`), flagged by `security-guidance@claude-code-plugins`: 1. **CSV formula injection inside the multi-line `Exécution` cell**: `_csv_safe` only checks `cell[0]`. With `executed_at` non-null, the cell starts with a safe date digit, but inner lines (commands, execution_result) starting with `=`/`+`/`-`/`@` evaded defense. Fix: `_format_execution_csv()` applies `_csv_safe` per user-controlled component BEFORE the multi-line concat. Outer `_csv_safe` on the assembled cell retained as belt-and-braces. 2. **Stored XSS in Markdown table cells**: the new GFM table allows inline HTML (we use it for `
`). A `sim.commands = ""` would be rendered raw by MD viewers that interpret inline HTML (Notion, Obsidian, GitHub preview). Fix: `_cell()` now calls `html.escape()` on each value BEFORE the pipe-escape and `\n` → `
` substitution — mirrors the `_render_engagement_html` PDF defense. The `
` we insert ourselves stays unescaped (it's not user-controlled). 2 dedicated regression tests added. +- 2026-06-09 (post-merge-review) — PDF export: A4 landscape orientation (user feedback post-merge-review). `@page { size: A4 landscape; }` added to `_CSS`; `font-size` reduced to 11px and `table-layout: fixed; word-break: break-word` added to prevent 7-column overflow on narrower portrait layout. --- diff --git a/backend/app/services/export.py b/backend/app/services/export.py index b0e830d..0b51e57 100644 --- a/backend/app/services/export.py +++ b/backend/app/services/export.py @@ -198,11 +198,12 @@ def render_engagement_csv( # --------------------------------------------------------------------------- _CSS = """ -body { font-family: sans-serif; font-size: 13px; color: #1a1a1a; margin: 40px; } -h1 { font-size: 22px; border-bottom: 2px solid #333; padding-bottom: 6px; } -h2 { font-size: 17px; margin-top: 32px; color: #333; } -table { border-collapse: collapse; width: 100%; margin-bottom: 12px; } -th, td { border: 1px solid #ccc; padding: 4px 8px; text-align: left; vertical-align: top; white-space: pre-wrap; } +@page { size: A4 landscape; margin: 20mm; } +body { font-family: sans-serif; font-size: 11px; color: #1a1a1a; margin: 0; } +h1 { font-size: 20px; border-bottom: 2px solid #333; padding-bottom: 6px; } +h2 { font-size: 15px; margin-top: 32px; color: #333; } +table { border-collapse: collapse; width: 100%; margin-bottom: 12px; table-layout: fixed; } +th, td { border: 1px solid #ccc; padding: 3px 6px; text-align: left; vertical-align: top; white-space: pre-wrap; word-break: break-word; } th { background: #e0e0e0; } .meta { color: #555; margin-bottom: 16px; } """ diff --git a/backend/tests/test_export_render.py b/backend/tests/test_export_render.py index 6d6b8a6..2bf2474 100644 --- a/backend/tests/test_export_render.py +++ b/backend/tests/test_export_render.py @@ -291,6 +291,15 @@ def test_render_engagement_pdf_contains_simulation_table(app) -> None: assert header in html, f"Expected French header '{header}' in HTML" +def test_render_engagement_html_has_landscape_page_rule(app) -> None: + from backend.app.services.export import _render_engagement_html + + with app.app_context(): + eng = _make_engagement() + html = _render_engagement_html(eng, []) + assert "landscape" in html, "HTML must include A4 landscape @page rule for PDF output" + + # --------------------------------------------------------------------------- # Defense-in-depth: filename header injection # ---------------------------------------------------------------------------