Fix lost form submissions on archives, the unlocked update finish and six more
Update Symfony to 7.4.23 (12 advisories in 8 packages). - A form can be sent from /beitraege, /kategorie/... and /schlagwort/...; it answered 404 before anything was stored or mailed. - Booleans reach the database as integers, so MySQL in strict mode accepts form submissions, task switches and plugin states. - A stepped form with a required multiple choice shows its message instead of stopping on a TypeError. - The second update step runs under the update lock; a concurrent finish no longer rolls a successful update back. - Accordion entries and gallery images can be selected from the editor panel again. - Category and tag names are escaped in the archive heading. - Restoring a published page from the bin needs the publisher role. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
441e629e80
commit
d464c82ac8
17 files changed
+324
-168
No files matched your search
@@ -311,6 +311,31 @@ final class ArchiveUpdateTest extends TestCase
|
||||
self::assertFileDoesNotExist($this->root.'/'.Updater::MAINTENANCE_FILE);
|
||||
}
|
||||
|
||||
public function testASecondFinishWhileOneRunsDoesNotSwitchBack(): void
|
||||
{
|
||||
// Meta-Refresh und der Link „Weiter, falls nichts passiert“ können den Abschluss zweimal
|
||||
// anstoßen. Der zweite darf nichts tun - vorher scheiterte er an der laufenden Migration
|
||||
// und schaltete ein gelungenes Update zurück.
|
||||
$this->releases->release(self::BASE, self::API, '1.1.0', $this->archive('1.1.0'));
|
||||
self::assertTrue($this->updater()->run()['ok']);
|
||||
$sperre = (new Updater($this->root, 'sqlite:///'.$this->root.'/var/cms.db', new CommandRunner(), $this->db))->lock();
|
||||
self::assertNotNull($sperre);
|
||||
|
||||
$zweiter = $this->updater()->finish($this->installation(), static fn (): string => 'würde zurückschalten');
|
||||
|
||||
self::assertTrue($zweiter['laeuft'] ?? false);
|
||||
self::assertSame('1.1.0', trim((string) file_get_contents($this->root.'/VERSION')), 'Der Code der neuen Fassung bleibt.');
|
||||
self::assertNotNull($this->updater()->pending(), 'Der offene Abschluss gehört weiter dem ersten Lauf.');
|
||||
self::assertSame('migrieren', $this->updater()->status()['phase']);
|
||||
|
||||
flock($sperre, LOCK_UN);
|
||||
fclose($sperre);
|
||||
// Wer die Sperre schon hält (der Abschluss aus der Ferne), läuft durch.
|
||||
$fertig = $this->updater()->finish($this->installation());
|
||||
self::assertTrue($fertig['ok'], $this->explain($fertig));
|
||||
self::assertNull($this->updater()->pending());
|
||||
}
|
||||
|
||||
public function testANewVersionThatDoesNotRunIsSwitchedBackCompletely(): void
|
||||
{
|
||||
// Wie bei reiseSoft: Läuft die neue Fassung nach den Migrationen nicht, kommt die alte
|
||||
|
||||
@@ -163,6 +163,39 @@ final class BacklogTest extends TestCase
|
||||
self::assertStringContainsString('Dieses Feld wird benötigt.', $antwort->getContent());
|
||||
}
|
||||
|
||||
public function testAFormInTheFooterCanBeSentFromAnArchive(): void
|
||||
{
|
||||
(new FormRepository($this->db))->save('kontakt', 'Kontakt', [
|
||||
['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true],
|
||||
['name' => 'datenschutz', 'type' => 'consent', 'label' => 'Einverstanden'],
|
||||
], ['recipient' => 'info@example.org'], 0);
|
||||
$footer = $this->partials->findByKind('footer');
|
||||
$doc = $footer['document'];
|
||||
$doc['schemaVersion'] = 23;
|
||||
$doc['blocks'][] = ['id' => 'fuss-formular', 'type' => 'form', 'props' => ['form' => 'kontakt']];
|
||||
$this->partials->save($footer['id'], $doc, $footer['version']);
|
||||
$this->publishHome(DemoContent::starter('Start'));
|
||||
$this->post('erster', 'Erster Beitrag', '2026-01-01 10:00', ['Neues']);
|
||||
self::assertStringContainsString('name="page" value="beitraege"', $this->request('/beitraege')->getContent());
|
||||
|
||||
// Ein Archiv ist keine gespeicherte Seite. Vorher endete das Absenden hier in einer 404,
|
||||
// bevor irgendetwas gespeichert oder verschickt war.
|
||||
$body = fn (string $page, string $email): array => ['page' => $page, 'block' => 'fuss-formular', 'issued' => (string) (time() - 10), 'issuedSignature' => FormRepository::stamp('kontakt', time() - 10, 'test-only'), 'email' => $email, 'datenschutz' => 'ja'];
|
||||
$fehler = $this->request('/formular/kontakt', 'POST', $body('beitraege', ''));
|
||||
self::assertSame(422, $fehler->getStatusCode());
|
||||
self::assertStringContainsString('Dieses Feld wird benötigt.', $fehler->getContent());
|
||||
self::assertStringContainsString('Erster Beitrag', $fehler->getContent(), 'Die Meldung steht im Archiv, nicht auf einer Fehlerseite.');
|
||||
|
||||
foreach (['beitraege', 'kategorie/neues'] as $n => $archiv) {
|
||||
$antwort = $this->request('/formular/kontakt', 'POST', $body($archiv, 'kunde'.$n.'@example.org'));
|
||||
self::assertSame(303, $antwort->getStatusCode());
|
||||
self::assertSame('/'.$archiv.'?gesendet=kontakt#fuss-formular', $antwort->headers->get('Location'));
|
||||
self::assertSame($n + 1, (int) $this->db->fetchOne('SELECT COUNT(*) FROM form_submission'), 'Die Anfrage ist gespeichert.');
|
||||
}
|
||||
self::assertSame(200, $this->request('/beitraege?gesendet=kontakt')->getStatusCode());
|
||||
self::assertSame(404, $this->request('/formular/kontakt', 'POST', $body('gibt-es-nicht', 'x@example.org'))->getStatusCode(), 'Eine unbekannte Seite bleibt eine unbekannte Seite.');
|
||||
}
|
||||
|
||||
public function testASectionLinkedTwiceRendersWithoutDuplicateIds(): void
|
||||
{
|
||||
$sektion = DemoContent::starter('Band');
|
||||
|
||||
@@ -931,6 +931,18 @@ final class CmsTest extends TestCase
|
||||
self::assertStringContainsString('href="https://cms.test/beitraege"', $html);
|
||||
}
|
||||
|
||||
public function testACategoryNameIsPrintedAsTextInItsArchiveHeading(): void
|
||||
{
|
||||
// Die Überschrift des Archivs läuft durch den Baustein „Überschrift“, der seinen Text als
|
||||
// Markup druckt. Ein Kategoriename ist aber ungefilterter Text eines Redakteurs.
|
||||
$beitrag = $this->publishedPost('markup', 'Markup', '2026-02-01', ['Bau & <img src="https://example.org/x.png"> Technik']);
|
||||
$href = $this->posts->termsFor($beitrag)['category'][0]['href'];
|
||||
|
||||
$html = $this->request($href)->getContent();
|
||||
self::assertStringNotContainsString('<img src="https://example.org/x.png">', $html);
|
||||
self::assertStringContainsString('Bau & <img src="https://example.org/x.png"> Technik</h1>', $html);
|
||||
}
|
||||
|
||||
public function testCategoriesAndTagsGroupPostsAndGetTheirOwnArchives(): void
|
||||
{
|
||||
$projekt = $this->publishedPost('kanttresor', 'KantTresor', '2026-02-01', ['Projekte'], ['WordPress', 'DSGVO']);
|
||||
|
||||
@@ -382,9 +382,23 @@ final class EditorSecurityTest extends WebTestCase
|
||||
$client->request('POST', '/admin/pages/'.$entwurf.'/move', ['_csrf_token' => $token, 'expectedVersion' => $version($entwurf), 'slug' => 'neuer-name']);
|
||||
self::assertSame('neuer-name', $this->db->fetchOne('SELECT path FROM page WHERE id = ?', [$entwurf]));
|
||||
|
||||
// Liegt die veröffentlichte Seite im Papierkorb, holt er sie nicht zurück: sie wäre sofort
|
||||
// wieder online. Seinen Entwurf dagegen schon.
|
||||
$pages->trashPage($live, $version($live));
|
||||
$pages->trashPage($entwurf, $version($entwurf));
|
||||
foreach ([$live, $entwurf] as $id) {
|
||||
$client->request('POST', '/admin/pages/'.$id.'/restore', ['_csrf_token' => $token]);
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
self::assertNotNull($this->db->fetchOne('SELECT deleted_at FROM page WHERE id = ?', [$live]), 'Ein Redakteur stellt keine veröffentlichte Seite wieder online.');
|
||||
self::assertNull($this->db->fetchOne('SELECT deleted_at FROM page WHERE id = ?', [$entwurf]));
|
||||
|
||||
// Wer veröffentlichen darf, darf auch das.
|
||||
$client = $this->login('publisher', $client);
|
||||
$token = $client->request('GET', '/admin')->filter('input[name="_csrf_token"]')->last()->attr('value');
|
||||
$client->request('POST', '/admin/pages/'.$live.'/restore', ['_csrf_token' => $token]);
|
||||
self::assertNull($this->db->fetchOne('SELECT deleted_at FROM page WHERE id = ?', [$live]));
|
||||
self::assertStringContainsString('wieder öffentlich', $client->followRedirect()->text());
|
||||
$client->request('POST', '/admin/pages/'.$live.'/move', ['_csrf_token' => $token, 'expectedVersion' => $version($live), 'slug' => 'umbenannt']);
|
||||
self::assertSame('umbenannt', $this->db->fetchOne('SELECT path FROM page WHERE id = ?', [$live]));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user