Files
KantSites/src/Command/RepairEntitiesCommand.php
T
kantarciandClaude Opus 5 f4ec0d9877 Make the entity repair a console command, not a script
scripts/ is on the exclusion list of build-release.php, so an installation
that runs from the release archive - the usual case on shared hosting -
would never have seen scripts/repair-doubled-entities.php. The 1.20.1 notes
told everyone to run it, which was only true for a Git working copy.

cms:repair:entities does the same work and ships with the application: a dry
run by default, --schreiben to apply, each change validated the way saving
validates it (pages with the heading rule, areas without, as in
PartialRepository). The script is gone; there is one way to do this now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 09:05:11 +02:00

153 lines
6.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Command;
use App\Content\DocumentValidator;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Räumt Texte auf, die vor 1.20.1 bei jedem Speichern eine Kodierungsstufe dazubekommen haben.
*
* `RichText::sanitise()` hat Text ohne ein einziges Tag nur escaped, statt ihn vorher zu
* dekodieren. Aus „Freudenstadt &amp; Umgebung“ wurde deshalb `&amp;amp;`, dann `&amp;amp;amp;`
* – bis die Entity sichtbar auf der Seite stand. Der Fehler ist behoben; dieser Befehl bringt in
* Ordnung, was schon in der Datenbank liegt.
*
* Angefasst wird, was in Gebrauch ist: die veröffentlichte und die Entwurfsfassung jeder Seite
* und die Bereiche. Ältere Fassungen bleiben Historie – wer eine zurückholt, speichert sie
* danach einmal, und der reparierte Kern schreibt sie richtig.
*/
#[AsCommand(name: 'cms:repair:entities', description: 'Repariert doppelt kodierte Texte (&amp;amp;) aus Fassungen vor 1.20.1.')]
final class RepairEntitiesCommand extends Command
{
public function __construct(private Connection $db, private DocumentValidator $validator)
{
parent::__construct();
}
protected function configure(): void
{
$this->addOption('schreiben', null, InputOption::VALUE_NONE, 'Die Korrekturen wirklich speichern. Ohne diese Option wird nur gezeigt, was sich ändern würde.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$schreiben = (bool) $input->getOption('schreiben');
$gesamt = 0;
$zeilen = [];
// Die aktiven Fassungen jeder Seite: veröffentlicht und Entwurf.
$aktive = [];
foreach ($this->db->fetchAllAssociative('SELECT id, path, published_revision_id, draft_revision_id FROM page') as $seite) {
foreach (['published_revision_id' => 'veröffentlicht', 'draft_revision_id' => 'Entwurf'] as $spalte => $was) {
if (!empty($seite[$spalte])) {
$aktive[(string) $seite[$spalte]] = '/'.$seite['path'].' ('.$was.')';
}
}
}
foreach ($aktive as $revision => $bezeichnung) {
$roh = $this->db->fetchOne('SELECT document FROM revision WHERE id = ?', [$revision]);
if (!is_string($roh)) {
continue;
}
$dokument = json_decode($roh, true, 512, JSON_THROW_ON_ERROR);
$zahl = 0;
$beispiele = [];
$dokument['blocks'] = $this->bausteine($dokument['blocks'] ?? [], $zahl, $beispiele);
if (0 === $zahl) {
continue;
}
$gesamt += $zahl;
$zeilen[] = [$bezeichnung, $zahl, implode(' · ', $beispiele)];
if ($schreiben) {
// Geprüft wie beim Speichern: eine Korrektur darf kein ungültiges Dokument hinterlassen.
$this->validator->validate($dokument, writing: true);
$this->db->executeStatement('UPDATE revision SET document = ? WHERE id = ?', [$this->json($dokument), $revision]);
}
}
foreach ($this->db->fetchAllAssociative('SELECT id, kind, name, document FROM partial') as $bereich) {
$dokument = json_decode((string) $bereich['document'], true, 512, JSON_THROW_ON_ERROR);
$zahl = 0;
$beispiele = [];
$dokument['blocks'] = $this->bausteine($dokument['blocks'] ?? [], $zahl, $beispiele);
if (0 === $zahl) {
continue;
}
$gesamt += $zahl;
$zeilen[] = ['Bereich '.$bereich['kind'].' „'.$bereich['name'].'“', $zahl, implode(' · ', $beispiele)];
if ($schreiben) {
// Ein Bereich traegt keine Ueberschrift - genau wie im PartialRepository.
$this->validator->validate($dokument, requireHeading: false, writing: true);
$this->db->executeStatement('UPDATE partial SET document = ? WHERE id = ?', [$this->json($dokument), $bereich['id']]);
}
}
if (0 === $gesamt) {
$io->success('Nichts zu reparieren: kein Text trägt eine doppelt kodierte Entity.');
return Command::SUCCESS;
}
$io->table(['Wo', 'Stellen', 'Beispiel'], $zeilen);
if ($schreiben) {
$io->success(sprintf('%d Stelle(n) repariert.', $gesamt));
return Command::SUCCESS;
}
$io->warning(sprintf('%d Stelle(n) gefunden. Mit --schreiben anwenden.', $gesamt));
return Command::SUCCESS;
}
/** @param array<int, array<string, mixed>> $liste */
private function bausteine(array $liste, int &$zahl, array &$beispiele): array
{
foreach ($liste as $index => $block) {
foreach (['text', 'caption', 'label', 'title', 'hint', 'provider'] as $feld) {
$wert = $block['props'][$feld] ?? null;
if (!is_string($wert) || '' === $wert) {
continue;
}
$neu = $this->entschachteln($wert);
if ($neu !== $wert) {
$liste[$index]['props'][$feld] = $neu;
++$zahl;
if (count($beispiele) < 3) {
$beispiele[] = mb_substr($wert, 0, 40).' → '.mb_substr($neu, 0, 40);
}
}
}
if (isset($block['children']) && is_array($block['children'])) {
$liste[$index]['children'] = $this->bausteine($block['children'], $zahl, $beispiele);
}
}
return $liste;
}
/** Entschachtelt so oft wie nötig: aus &amp;amp;amp; wird &amp;. Ein einzelnes &amp; ist richtig und bleibt. */
private function entschachteln(string $text): string
{
$vorher = null;
while ($vorher !== $text) {
$vorher = $text;
$text = preg_replace('/&amp;(amp|lt|gt|quot|apos|nbsp|#\d{1,6}|#x[0-9a-fA-F]{1,6});/', '&$1;', $text) ?? $text;
}
return $text;
}
/** @param array<string, mixed> $dokument */
private function json(array $dokument): string
{
return json_encode($dokument, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}