Seiten im Browser anlegen und löschen, Adresshierarchie und Beitragstyp
Migration 20260912000400 ergänzt page.path, parent_id, type und
created_at. Die websiteweite Eindeutigkeit wandert vom Slug auf den
vollständigen Pfad, damit gleiche Segmente unter verschiedenen Eltern
möglich sind. Nur ADD COLUMN: Index- oder Fremdschlüsseländerungen
würden SQLite die Tabelle neu aufbauen lassen, was am Fremdschlüssel
revision.page_id scheitert; die Indizes werden in postUp() gesetzt.
POST /admin/pages legt Seiten und Beiträge als privaten Entwurf an
(ROLE_EDITOR), POST /admin/pages/{id}/delete entfernt eine Seite mit
allen Revisionen (ROLE_ADMIN). Beides mit CSRF, ohne JavaScript
bedienbar, mit Schutz für Elternseiten und veraltete Versionen.
Die öffentliche Route bedient verschachtelte Pfade und lässt die
CMS-Routen unberührt.
Testumgebung: Login-Throttling zählt jetzt in einem Array-Cache,
weil die persistierten Zähler die Anmeldung über Testläufe hinweg
aussperrten.
Geprüft: 56 PHP-Tests / 212 Assertions unter PHP 8.2.12 und 8.4.22,
10 JavaScript-Modelltests, Lint/Format/Container, Migration zuerst
gegen eine Datenbankkopie. Keine Browser- oder Lighthouse-Läufe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82b07820bc
commit
a6ffb9bdfd
16 files changed
+555
-23
No files matched your search
@@ -13,6 +13,7 @@ use Doctrine\DBAL\Schema\Schema;
|
||||
use DoctrineMigrations\Version20260912000100;
|
||||
use DoctrineMigrations\Version20260912000200;
|
||||
use DoctrineMigrations\Version20260912000300;
|
||||
use DoctrineMigrations\Version20260912000400;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
@@ -42,11 +43,13 @@ final class EditorSecurityTest extends WebTestCase
|
||||
(new Version20260912000100($this->db, new NullLogger()))->up($schema);
|
||||
(new Version20260912000200($this->db, new NullLogger()))->up($schema);
|
||||
(new Version20260912000300($this->db, new NullLogger()))->up($schema);
|
||||
(new Version20260912000400($this->db, new NullLogger()))->up($schema);
|
||||
foreach ($schema->toSql($this->db->getDatabasePlatform()) as $sql) {
|
||||
$this->db->executeStatement($sql);
|
||||
}
|
||||
Version20260912000400::applyIndexes($this->db);
|
||||
$this->db->insert('website', ['id' => 'default', 'name' => 'Test', 'default_locale' => 'de', 'theme' => '{}']);
|
||||
$this->db->insert('page', ['id' => 'p1', 'slug' => 'home', 'locale' => 'de', 'website_id' => 'default', 'version' => 0]);
|
||||
$this->db->insert('page', ['id' => 'p1', 'slug' => 'home', 'path' => 'home', 'type' => 'page', 'locale' => 'de', 'website_id' => 'default', 'version' => 0, 'created_at' => gmdate('Y-m-d H:i:s')]);
|
||||
$pages = new PageRepository($this->db, new DocumentValidator());
|
||||
$revision = $pages->saveDraft('p1', DemoContent::pages()['home'], 0);
|
||||
$pages->publish('p1', $revision, 1);
|
||||
@@ -107,6 +110,54 @@ final class EditorSecurityTest extends WebTestCase
|
||||
$this->post($client, $action, []);
|
||||
self::assertResponseRedirects('http://localhost/admin/login');
|
||||
}
|
||||
foreach ([['/admin/pages', ['title' => 'Geschmuggelt', 'slug' => 'geschmuggelt']], ['/admin/pages/p1/delete', ['expectedVersion' => 2]]] as [$url, $body]) {
|
||||
$client->request('POST', $url, $body);
|
||||
self::assertResponseRedirects('http://localhost/admin/login');
|
||||
}
|
||||
self::assertSame(1, (int) $this->db->fetchOne('SELECT COUNT(*) FROM page'));
|
||||
}
|
||||
|
||||
public function testCreatingAPageNeedsACsrfTokenAndStartsAsAPrivateDraft(): void
|
||||
{
|
||||
$client = $this->login('editor');
|
||||
$client->request('POST', '/admin/pages', ['title' => 'Ohne Token', 'slug' => 'ohne-token', '_csrf_token' => 'wrong']);
|
||||
self::assertResponseRedirects('/admin');
|
||||
self::assertSame(1, (int) $this->db->fetchOne('SELECT COUNT(*) FROM page'));
|
||||
|
||||
$crawler = $client->request('GET', '/admin');
|
||||
$client->submit($crawler->filter('.create-form')->form(['title' => 'Über uns', 'slug' => '', 'type' => 'page', 'parent_id' => '']));
|
||||
self::assertResponseRedirects();
|
||||
// The address is derived from the title when the field is left empty.
|
||||
self::assertSame('ueber-uns', $this->db->fetchOne('SELECT path FROM page WHERE slug <> ?', ['home']));
|
||||
// A new page is a draft, so it must not answer publicly yet.
|
||||
$client->request('GET', '/ueber-uns');
|
||||
self::assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function testEditorsAreNeitherOfferedNorAllowedToDelete(): void
|
||||
{
|
||||
$client = $this->login('editor');
|
||||
$crawler = $client->request('GET', '/admin');
|
||||
self::assertCount(0, $crawler->filter('.delete-panel'), 'An editor must not be offered the delete action.');
|
||||
$client->request('POST', '/admin/pages/p1/delete', ['_csrf_token' => $crawler->filter('input[name="_csrf_token"]')->last()->attr('value'), 'expectedVersion' => 2]);
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
self::assertSame(1, (int) $this->db->fetchOne('SELECT COUNT(*) FROM page'));
|
||||
}
|
||||
|
||||
public function testAdministratorsDeleteAPageWithAllRevisionsOnlyWithACsrfToken(): void
|
||||
{
|
||||
$client = $this->login('admin');
|
||||
$client->request('POST', '/admin/pages/p1/delete', ['_csrf_token' => 'wrong', 'expectedVersion' => 2]);
|
||||
self::assertResponseRedirects('/admin');
|
||||
self::assertSame(1, (int) $this->db->fetchOne('SELECT COUNT(*) FROM page'));
|
||||
|
||||
$crawler = $client->request('GET', '/admin');
|
||||
$client->submit($crawler->filter('.delete-panel form')->form());
|
||||
self::assertResponseRedirects('/admin');
|
||||
self::assertSame(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM page'));
|
||||
self::assertSame(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM revision'));
|
||||
$client->request('GET', '/');
|
||||
self::assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function testMediaUploadReencodingCsrfMetadataConflictAndArchive(): void
|
||||
|
||||
Reference in new issue
Block a user