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

+60
View File
@@ -12,6 +12,7 @@ use App\Content\PostRepository;
use App\Content\SiteSettings;
use App\Content\Theme;
use App\Domain\InvalidDocument;
use App\Domain\StaleWrite;
use App\Infrastructure\ConnectionFactory;
use App\Kernel;
use App\Ops\ArchiveUpdate;
@@ -163,6 +164,65 @@ final class BacklogTest extends TestCase
self::assertStringContainsString('Dieses Feld wird benötigt.', $antwort->getContent());
}
public function testAPageWhoseStoredCssNoLongerPassesStaysReachableAndEditable(): void
{
// Gespeichert wurde, was beim Speichern galt. Ist eine Regel inzwischen strenger, fällt das
// CSS dieses einen Blocks weg (ADR 116) - die Seite antwortet nicht mit 500 und lässt sich
// zum Reparieren öffnen. Vorher warf schon das Lesen.
$document = DemoContent::starter('Altbestand');
$document['blocks'][] = ['id' => 'alt-text', 'type' => 'text', 'props' => ['text' => 'Bleibt lesbar'], 'css' => 'color: red;'];
$this->publishHome($document);
$alt = '& ~ p { color: red }';
try {
(new DocumentValidator())->validate([...$document, 'schemaVersion' => 23, 'blocks' => [...array_slice($document['blocks'], 0, -1), ['id' => 'alt-text', 'type' => 'text', 'props' => ['text' => 'Bleibt lesbar'], 'css' => $alt]]], writing: true);
self::fail('Dieses CSS muss beim Speichern abgelehnt werden, sonst prüft der Test nichts.');
} catch (InvalidDocument) {
}
foreach ($this->db->fetchAllAssociative('SELECT id, document FROM revision WHERE page_id = ?', ['page-1']) as $fassung) {
$this->db->update('revision', ['document' => str_replace('color: red;', $alt, $fassung['document'])], ['id' => $fassung['id']]);
}
$antwort = $this->request('/');
self::assertSame(200, $antwort->getStatusCode());
self::assertStringContainsString('Bleibt lesbar', $antwort->getContent());
self::assertStringNotContainsString('~ p', $antwort->getContent(), 'Das abgelehnte CSS wird nicht ausgegeben.');
$entwurf = $this->pages->findForEditor('page-1');
self::assertSame($alt, end($entwurf['document']['blocks'])['css'], 'Im Editor steht es noch da und lässt sich korrigieren.');
// Speichern verlangt weiter gültiges CSS.
$this->expectException(InvalidDocument::class);
$this->pages->saveDraft('page-1', $entwurf['document'], $entwurf['version']);
}
public function testSettingsAndThemeRefuseAWriteFromAnOutdatedForm(): void
{
// Zwei Admins haben dasselbe Formular offen. Der zweite überschrieb bisher still, was der
// erste gespeichert hatte. Jetzt trägt das Formular eine Kennung des geladenen Stands.
$werte = ['legalName' => 'Erste GmbH'];
$geladen = $this->site->stamp();
$this->site->save($werte, $geladen);
self::assertNotSame($geladen, $this->site->stamp());
try {
$this->site->save([...$werte, 'legalName' => 'Zweite GmbH'], $geladen);
self::fail('Der veraltete Stand muss abgelehnt werden.');
} catch (StaleWrite) {
}
self::assertSame('Erste GmbH', $this->site->load()['legalName']);
$this->site->save([...$werte, 'legalName' => 'Zweite GmbH'], $this->site->stamp());
self::assertSame('Zweite GmbH', $this->site->load()['legalName']);
$theme = new Theme($this->db);
$stand = $theme->load();
$geladen = $theme->stamp();
$theme->save($stand['colors'], $stand['fonts'], [...$stand['base'], 'linkUnderline' => null], null, $geladen);
try {
$theme->save($stand['colors'], $stand['fonts'], $stand['base'], null, $geladen);
self::fail('Der veraltete Stand muss abgelehnt werden.');
} catch (StaleWrite) {
}
self::assertFalse($theme->load()['base']['linkUnderline'], 'Die erste Änderung bleibt.');
}
public function testAFormInTheFooterCanBeSentFromAnArchive(): void
{
(new FormRepository($this->db))->save('kontakt', 'Kontakt', [