Keep old pages readable, detect stale settings writes, make backups honest

- Own CSS is parsed on write only. A page whose stored CSS no longer
  passes a stricter rule answers 200 and opens in the editor; that one
  block loses its CSS. A document read several times in one request is
  validated once.
- Settings and theme refuse a write from an outdated form.
- A backup fails when a media file does not arrive completely and
  leaves no half directory behind; rotation counts intact backups only.
- The archive update takes its backup behind the maintenance page.
- A package import puts all media in place before removing the old
  ones, reports honestly when that fails and keeps a maintenance state
  that was already on.
- Restoring replaces the SQLite file in one move and points at pending
  migrations.
- Six irreversible actions ask first, in a dialog of the admin's own.
- Ctrl+S in the editor takes the field being typed in along; Delete
  no longer removes a block while a dialog is open.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.2 committed 2026-11-10 11:41:05 +01:00
1 parent 0d61aa0f22
commit ead47cc924
25 files changed
+437 -72

No files matched your search

+33 -2
View File
@@ -9,6 +9,9 @@ final class DocumentValidator
{
private bool $writing = false;
/** @var array<string, true> beim Lesen bereits geprüfte Dokumente dieser Anfrage */
private array $passed = [];
public function __construct(private StyleCompiler $styles = new StyleCompiler(), private CustomCode $code = new CustomCode(), private ?PluginRegistry $plugins = null)
{
}
@@ -59,6 +62,24 @@ final class DocumentValidator
* lässt unpassende Eigenschaften ohnehin nicht durch.
*/
public function validate(array $document, bool $requireHeading = true, bool $writing = false): void
{
// Beim Lesen wird dasselbe Dokument in einer Anfrage oft mehrfach geholt (Seite,
// Brotkrumen, strukturierte Daten). Geprüft wird es einmal; der Schlüssel ist sein Inhalt,
// ein geändertes Dokument ist also ein anderes.
$seen = null;
if (!$writing) {
$seen = hash('xxh128', serialize($document)).($requireHeading ? '1' : '0');
if (isset($this->passed[$seen])) {
return;
}
}
$this->check($document, $requireHeading, $writing);
if (null !== $seen) {
$this->passed[$seen] = true;
}
}
private function check(array $document, bool $requireHeading, bool $writing): void
{
$this->writing = $writing;
$schemaVersion = $document['schemaVersion'] ?? null;
@@ -133,7 +154,7 @@ final class DocumentValidator
$this->require(is_string($key) && strlen($key) <= 110 && is_string($hash) && 1 === preg_match('/^[a-f0-9]{8}$/D', $hash), 'Ungültiger Vermerk zur Übersetzung.');
}
try {
$this->validate(Translations::apply($document, $locale), $requireHeading, $this->writing);
$this->check(Translations::apply($document, $locale), $requireHeading, $this->writing);
} catch (InvalidDocument $error) {
throw new InvalidDocument(sprintf('%s (%s): %s', Languages::CATALOG[$locale]['name'], $locale, $error->getMessage()), previous: $error);
}
@@ -171,7 +192,17 @@ final class DocumentValidator
// and is answered in the controller, not here.
$this->require(CustomCode::supports($schemaVersion), 'Own code requires schema v13.');
$this->require(is_string($block[$own]), sprintf('"%s" must be text.', $own));
'css' === $own ? $this->code->checkCss($block[$own]) : $this->code->checkScript($block[$own]);
// Eigenes CSS wird nur beim Schreiben geparst. Was gespeichert ist, galt beim Speichern;
// ist eine Regel inzwischen strenger, lässt compileCss() das CSS dieses einen Blocks
// weg (ADR 116). Hier beim Lesen zu werfen hieße: die Seite antwortet mit 500 und lässt
// sich nicht einmal mehr zum Reparieren öffnen.
if ('css' === $own) {
if ($this->writing) {
$this->code->checkCss($block[$own]);
}
} else {
$this->code->checkScript($block[$own]);
}
}
if (isset($block['style'])) {
// Free styling: any property of StyleSchema, per device, with an optional hover value.