Add an own-HTML block, and build the DEEG offers page on the embed block

Umut asked for a block that takes custom HTML, and for an iframe on a new
DEEG offers page. Only one of those was missing.

New core block "Eigenes HTML" (document schema v24): one property, the
markup, up to 20.000 characters, written to the page unchanged. Changing it
is reserved for administrators and compared against what is stored - on
saving, on saving an area, and on restoring an older revision - exactly like
an own script. <script> and <iframe> are refused with the way to the right
place instead of being swallowed by the content security policy: scripts
belong in the script field, frames in the "Externer Inhalt" block, because
only that one asks for consent and puts the host into frame-src (ADR 106).

DEEG: /angebote existed as a draft with placeholder text. It is now built
like the legal pages - small caps line, large light heading, lead on the
tinted band - with the IS24 result list below it and a line pointing to the
portal and to the contact form. Navigation and the offers band on the home
page point there. The consent question is switched on, so the frame loads
only after the visitor agrees, and both the banner and the placeholder carry
the site's own shapes and colours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-16 08:27:43 +02:00
1 parent 6b1aa50d02
commit 59a40157a4
15 files changed
+465 -8

No files matched your search

+73
View File
@@ -3111,6 +3111,79 @@ final class CmsTest extends TestCase
$code->assertScriptsUnchanged($added, $before);
}
public function testOwnHtmlReachesThePageUnchangedAndRefusesScriptAndFrame(): void
{
// Der Baustein „Eigenes HTML“ (Schema v24): Markup des Betreibers, unverändert auf der
// Seite. Zwei Tags nimmt er nicht an, weil die Sicherheitsrichtlinie sie ohnehin nicht
// ausführt bzw. nicht lädt - dafür gibt es das Skriptfeld und den Baustein „Externer
// Inhalt“.
$id = $this->publishedPage('angebote', 'Angebote');
$document = $this->pages->findForEditor($id)['document'];
$document['schemaVersion'] = 24;
$document['blocks'][0]['children'][] = ['id' => 'roh', 'type' => 'html', 'props' => ['html' => '<table class="preise"><tr><td>Miete</td><td>ab 8 €/m²</td></tr></table>']];
$revision = $this->pages->saveDraft($id, $document, $this->pages->findForEditor($id)['version']);
$this->pages->publish($id, $revision, $this->pages->findForEditor($id)['version']);
$html = $this->request('/angebote')->getContent();
self::assertStringContainsString('<div id="roh" class="block-html">', $html);
// Unverändert heißt unverändert: keine Entschärfung, keine Umschrift.
self::assertStringContainsString('<table class="preise"><tr><td>Miete</td><td>ab 8 €/m²</td></tr></table>', $html);
foreach ([
'<script>alert(1)</script>' => 'Skript',
'<iframe src="https://example.com"></iframe>' => 'Rahmen',
'< script >alert(1)</script>' => 'Skript mit Leerzeichen',
] as $markup => $fall) {
$attempt = $document;
$attempt['blocks'][0]['children'][2]['props']['html'] = $markup;
try {
$this->pages->saveDraft($id, $attempt, $this->pages->findForEditor($id)['version']);
self::fail(sprintf('%s hätte abgewiesen werden müssen.', $fall));
} catch (InvalidDocument) {
}
}
// Vor v24 gibt es den Baustein nicht.
$alt = $document;
$alt['schemaVersion'] = 23;
try {
$this->pages->saveDraft($id, $alt, $this->pages->findForEditor($id)['version']);
self::fail('Eigenes HTML vor Schema v24 hätte abgewiesen werden müssen.');
} catch (InvalidDocument) {
}
}
public function testOwnHtmlChangesAreComparedAgainstWhatIsStored(): void
{
// Dieselbe Rolle wie beim eigenen Skript: ändern darf es nur ein Administrator, und
// geprüft wird gegen den gespeicherten Stand - ein Redakteur kann die Seite also weiter
// speichern, ohne das HTML anfassen zu können.
$code = new CustomCode();
$stored = DemoContent::starter('Mit HTML');
$stored['schemaVersion'] = 24;
$stored['blocks'][0]['children'][] = ['id' => 'roh', 'type' => 'html', 'props' => ['html' => '<p>Original</p>']];
$unchanged = $stored;
$unchanged['title'] = 'Anderer Titel';
$code->assertHtmlUnchanged($unchanged, $stored);
foreach (['geändert' => '<p>Fremd</p>', 'geleert' => ''] as $fall => $markup) {
$attempt = $stored;
$attempt['blocks'][0]['children'][2]['props']['html'] = $markup;
try {
$code->assertHtmlUnchanged($attempt, $stored);
self::fail(sprintf('HTML %s: hätte abgewiesen werden müssen.', $fall));
} catch (InvalidDocument) {
}
}
// Und neu hinzufügen ebenso wenig.
$ohne = DemoContent::starter('Ohne HTML');
$ohne['schemaVersion'] = 24;
$this->expectException(InvalidDocument::class);
$code->assertHtmlUnchanged($stored, $ohne);
}
/** Turns on the consent question and publishes a privacy page it can point to. */
private function askConsent(array $groups = ['external']): void
{