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> $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; wird &. Ein einzelnes & ist richtig und bleibt. */ private function entschachteln(string $text): string { $vorher = null; while ($vorher !== $text) { $vorher = $text; $text = preg_replace('/&(amp|lt|gt|quot|apos|nbsp|#\d{1,6}|#x[0-9a-fA-F]{1,6});/', '&$1;', $text) ?? $text; } return $text; } /** @param array $dokument */ private function json(array $dokument): string { return json_encode($dokument, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } }