Stop text from being encoded again on every save

Umut: "Beim Startseitenbild wird der Text & umgewandelt, aber in der Ansicht
nach dem Speichern wird & daraus."

RichText::sanitise() treated text without a single tag as plain text and only
escaped it. What the editor sends is HTML, tag or no tag - so "Freudenstadt
& Umgebung" gained one level of encoding on every save (&,
& ...) until the entity stood on the page for everyone to read.
The draft of the kant-it.de home page had already reached three levels.

The same branch carried a second fault of the same kind: nl2br() left the
newline behind the <br>, and the next pass turned it into a second <br> via
breakLines() - one paragraph became two. The branch now decodes first and
replaces the line break instead of adding to it, so it ends up with the same
representation as the DOM branch and both are stable under repeated saves.

scripts/repair-doubled-entities.php cleans up what is already stored: the
published and draft revision of every page and the areas, with a dry run,
each change checked against the validator. Older revisions stay as history.

DEEG: favicon is now its own square mark - the roof gable from the logo,
white on the logo blue, rendered from logo-deeg.png - instead of the centre
crop of the wide logo, which showed the "EE" of the wordmark.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-16 08:57:20 +02:00
1 parent d885b3ddb5
commit 4539f357c1
6 files changed
+297 -1

No files matched your search

+40
View File
@@ -3111,6 +3111,46 @@ final class CmsTest extends TestCase
$code->assertScriptsUnchanged($added, $before);
}
public function testSavingTextTwiceKeepsAmpersandsAndLineBreaksAsTheyAre(): void
{
// Umut: „Beim Startseitenbild wird & umgewandelt, aber nach dem Speichern steht &amp;
// da.“ Der Editor schickt HTML - auch ein Text ohne ein einziges Tag. Wurde der nur
// escaped statt vorher dekodiert, wuchs die Entity bei jedem Speichern um eine Stufe,
// bis sie sichtbar auf der Seite stand. Dasselbe galt fuer den Zeilenumbruch: nl2br()
// liess das \n hinter dem <br> stehen, und der zweite Durchlauf machte zwei daraus.
$text = new RichText();
foreach ([
'Freudenstadt &amp; Umgebung' => 'Freudenstadt &amp; Umgebung',
'Haus & Hof' => 'Haus &amp; Hof',
'5 < 7 & mehr' => '5 &lt; 7 &amp; mehr',
"Zeile eins\nZeile zwei" => 'Zeile eins<br>Zeile zwei',
'<p>A &amp; B</p>' => '<p>A &amp; B</p>',
'&lt;b&gt;woertlich&lt;/b&gt;' => '&lt;b&gt;woertlich&lt;/b&gt;',
] as $eingabe => $erwartet) {
$einmal = $text->sanitise($eingabe);
self::assertSame($erwartet, $einmal, $eingabe);
// Und jedes weitere Speichern laesst es, wie es ist.
self::assertSame($einmal, $text->sanitise($einmal), 'zweites Speichern: '.$eingabe);
self::assertSame($einmal, $text->sanitise($text->sanitise($einmal)), 'drittes Speichern: '.$eingabe);
}
// Und durch das ganze Dokument, so wie es beim Speichern einer Seite passiert.
$document = DemoContent::starter('Kaufmaennisches Und');
$document['schemaVersion'] = 17;
$document['blocks'][0]['children'][0]['props']['text'] = 'Freudenstadt &amp; Umgebung';
$document['blocks'][0]['children'][1]['props']['text'] = 'Immobilien & Verwaltung';
$id = $this->publishedPage('und', 'Und');
for ($runde = 0; $runde < 3; ++$runde) {
$stand = $this->pages->findForEditor($id);
$this->pages->publish($id, $this->pages->saveDraft($id, $document, $stand['version']), $this->pages->findForEditor($id)['version']);
$document = $this->pages->findForEditor($id)['document'];
}
$html = $this->request('/und')->getContent();
self::assertStringContainsString('Freudenstadt &amp; Umgebung', $html);
self::assertStringContainsString('Immobilien &amp; Verwaltung', $html);
self::assertStringNotContainsString('&amp;amp;', $html, 'Nach drei Speichervorgaengen steht immer noch ein einziges Und da.');
}
public function testOwnHtmlReachesThePageUnchangedAndRefusesScriptAndFrame(): void
{
// Der Baustein „Eigenes HTML“ (Schema v24): Markup des Betreibers, unverändert auf der