Rebuild DEEG as a fifth installation, add stepped forms and a plugin system, and clear the review backlog
Forms gained steps, conditions, image choices, number fields with units and a summary step; the DEEG price finder uses all of it. Plugins are manifest plus Twig: blocks, settings, containers, routes, presets and theme packs, switched per installation. The review backlog is done: archive pages render through the normal page pipeline, forms and post lists work inside areas and sections, repeated sections get their own ids, breakpoints and colours come from the palette, backup and update work on MySQL and lock each other out. The task worker no longer shortens a process that has no time limit: set_time_limit() sets the limit instead of extending it, which turned an unlimited CLI run into eighty seconds and killed the full test suite far away from the scheduler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
66ff68abf8
commit
c8a8fa7460
74 files changed
+5352
-243
No files matched your search
@@ -3,10 +3,13 @@
|
||||
namespace App\Content;
|
||||
|
||||
use App\Domain\InvalidDocument;
|
||||
use App\Plugin\PluginRegistry;
|
||||
|
||||
final class DocumentValidator
|
||||
{
|
||||
public function __construct(private StyleCompiler $styles = new StyleCompiler(), private CustomCode $code = new CustomCode())
|
||||
private bool $writing = false;
|
||||
|
||||
public function __construct(private StyleCompiler $styles = new StyleCompiler(), private CustomCode $code = new CustomCode(), private ?PluginRegistry $plugins = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -35,16 +38,24 @@ final class DocumentValidator
|
||||
public const V15_TYPES = ['icon', 'progress', 'slider'];
|
||||
/** Schema v23: die sozialen Profile aus den Einstellungen, als Symbolreihe an jeder Stelle. */
|
||||
public const V23_TYPES = ['social'];
|
||||
/** Die aktuelle Fassung des Dokumentschemas - dieselbe Zahl wie SCHEMA_VERSION in editor-model.js. */
|
||||
public const SCHEMA_VERSION = 23;
|
||||
|
||||
/**
|
||||
* @param bool $requireHeading A page needs exactly one main heading. A header, a footer or a
|
||||
* reusable section that appears on many pages must not bring one
|
||||
* of its own, or every page carrying it would have two.
|
||||
* @param bool $writing Beim Schreiben werden die Eigenschaften von Plugin-Bausteinen
|
||||
* gegen ihr Schema geprüft. Beim Lesen nicht: ein Plugin, das
|
||||
* nach dem Speichern eingeschaltet oder geändert wurde, darf
|
||||
* eine gespeicherte Seite nicht unlesbar machen - der Renderer
|
||||
* lässt unpassende Eigenschaften ohnehin nicht durch.
|
||||
*/
|
||||
public function validate(array $document, bool $requireHeading = true): void
|
||||
public function validate(array $document, bool $requireHeading = true, bool $writing = false): void
|
||||
{
|
||||
$this->writing = $writing;
|
||||
$schemaVersion = $document['schemaVersion'] ?? null;
|
||||
$this->require(in_array($schemaVersion, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], true), 'Unsupported document schema.');
|
||||
$this->require(is_int($schemaVersion) && $schemaVersion >= 1 && $schemaVersion <= self::SCHEMA_VERSION, 'Unsupported document schema.');
|
||||
$this->text($document['title'] ?? null, 160);
|
||||
$this->text($document['description'] ?? null, 320);
|
||||
$this->require(is_array($document['blocks'] ?? null) && array_is_list($document['blocks']), 'Blocks must be a list.');
|
||||
@@ -112,6 +123,21 @@ final class DocumentValidator
|
||||
$props = $block['props'] ?? [];
|
||||
$layout = $block['layout'] ?? [];
|
||||
$this->require(is_array($props) && is_array($layout), 'Props and layout must be objects.');
|
||||
// Ein aktives Plugin kennt das Schema seines Bausteins - dann wird es auch geprüft, so
|
||||
// streng wie bei einem Kernbaustein. Ohne Plugin bleibt der Erhaltungsvertrag: aufbewahren,
|
||||
// nicht prüfen, nicht rendern.
|
||||
if ($plugin && $this->writing && null !== $this->plugins && null !== ($definition = $this->plugins->block($type))) {
|
||||
$this->plugins->validateProps($type, $props);
|
||||
// Ein Container nimmt Kinder auf, ein gewöhnlicher Baustein nicht; eine Liste
|
||||
// erlaubter Kinder im Manifest gilt wie ALLOW bei Galerie oder Akkordeon.
|
||||
$kinder = is_array($block['children'] ?? null) ? $block['children'] : [];
|
||||
$this->require($definition['container'] || [] === $kinder, sprintf('„%s“ nimmt keine Bausteine auf.', $definition['label']));
|
||||
if (null !== $definition['children']) {
|
||||
foreach ($kinder as $kind) {
|
||||
$this->require(in_array($kind['type'] ?? '', $definition['children'], true), sprintf('In „%s“ passt nur: %s.', $definition['label'], implode(', ', $definition['children'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($layout as $key => $value) {
|
||||
if ('visibility' === $key) {
|
||||
$this->require($schemaVersion >= 22 && in_array($value, ['all', 'desktop', 'tablet', 'mobile', 'hide-desktop', 'hide-tablet', 'hide-mobile', 'hidden'], true), 'Unbekannte Sichtbarkeit oder Dokumentschema vor v22.');
|
||||
@@ -240,7 +266,9 @@ final class DocumentValidator
|
||||
foreach (['phone', 'email', 'address'] as $field) {
|
||||
$this->text($props[$field] ?? null, 'address' === $field ? 200 : 80, true);
|
||||
}
|
||||
$this->require('' !== trim($props['phone'].$props['email'].$props['address']), 'A contact block needs at least one detail.');
|
||||
// Leer ist erlaubt: der Baustein steht nach dem Einfuegen erst einmal ohne Angaben da,
|
||||
// und ein Dokument mit einem leeren Kontaktblock darf sich speichern lassen. Oeffentlich
|
||||
// rendert er dann nichts, im Editor einen Hinweis.
|
||||
if ('' !== $props['phone']) {
|
||||
$this->require(1 === preg_match('/^\+?[0-9 ()\/-]{3,32}$/D', $props['phone']), 'Invalid phone number.');
|
||||
}
|
||||
@@ -263,6 +291,10 @@ final class DocumentValidator
|
||||
if ('tabitem' === $type) {
|
||||
$this->text($props['title'] ?? null, 160);
|
||||
}
|
||||
if ('legal' === $type) {
|
||||
// Die Beschriftung der Rechtszeile ist der Name der Landmarke fuer Vorlesesoftware.
|
||||
$this->text($props['label'] ?? 'Rechtliches', 60, true);
|
||||
}
|
||||
if ('menu' === $type) {
|
||||
// The entries live in the menu administration; the block only says which slot to show
|
||||
// and how to lay it out.
|
||||
@@ -283,7 +315,7 @@ final class DocumentValidator
|
||||
if ('partialref' === $type) {
|
||||
// Only the address of the section is content. Its blocks live in the partial, so one
|
||||
// change reaches every page that links it and a page cannot alter a shared section.
|
||||
$this->require(is_string($props['partial'] ?? null) && 1 === preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/D', $props['partial']) && strlen($props['partial']) <= 80, 'Invalid section address.');
|
||||
$this->require(is_string($props['partial'] ?? null) && ('' === $props['partial'] || (1 === preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/D', $props['partial']) && strlen($props['partial']) <= 80)), 'Invalid section address.');
|
||||
}
|
||||
if ('icon' === $type) {
|
||||
$this->require(in_array($props['icon'] ?? null, self::ICONS, true), 'Unknown icon.');
|
||||
@@ -338,7 +370,9 @@ final class DocumentValidator
|
||||
$this->require(is_bool($props['dots']), 'Die Punkte sind ja oder nein.');
|
||||
}
|
||||
}
|
||||
if ('embed' === $type) {
|
||||
if ('embed' === $type && '' !== ($props['url'] ?? '')) {
|
||||
// Ohne Adresse ist der Baustein ein leerer Rahmen, den der Editor benennt - erst mit
|
||||
// Adresse gelten Titel, Anbieter und Hoehe.
|
||||
// Everything about the embed is validated here, because the address ends up in an
|
||||
// iframe and in the content security policy of the page.
|
||||
$this->require(isset(Consent::GROUPS[$props['group'] ?? '']), 'Unbekannte Einwilligungsgruppe.');
|
||||
@@ -358,7 +392,8 @@ final class DocumentValidator
|
||||
if ('form' === $type) {
|
||||
// Only the address is content. Fields, recipient and consent text stay in the database,
|
||||
// so a stored document can never redirect a submission or change what is asked.
|
||||
$this->require(is_string($props['form'] ?? null) && 1 === preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/D', $props['form']) && strlen($props['form']) <= 80, 'Invalid form address.');
|
||||
// Leer heisst: noch kein Formular gewaehlt. Der Editor zeigt das, die Website nichts.
|
||||
$this->require(is_string($props['form'] ?? null) && ('' === $props['form'] || (1 === preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/D', $props['form']) && strlen($props['form']) <= 80)), 'Invalid form address.');
|
||||
}
|
||||
if ('gallery' === $type && isset($props['lightbox'])) {
|
||||
$this->require($schemaVersion >= 19, 'Der Bildbetrachter braucht Schema v19.');
|
||||
@@ -432,7 +467,9 @@ final class DocumentValidator
|
||||
foreach ($design as $device => $settings) {
|
||||
$this->require(in_array($device, ['desktop', 'tablet', 'mobile'], true) && is_array($settings), 'Invalid responsive device.');
|
||||
foreach ($settings as $key => $value) {
|
||||
$this->require(isset($tokens[$key]) && in_array($type, $tokens[$key]['types'], true), 'Design setting does not apply to this block.');
|
||||
// Plugin-Bausteine dürfen jedes Token tragen: die Klassen dafür sind allgemein, und
|
||||
// das Manifest kann nicht in dieser Datei stehen.
|
||||
$this->require(isset($tokens[$key]) && (in_array($type, $tokens[$key]['types'], true) || str_contains($type, '.')), 'Design setting does not apply to this block.');
|
||||
$this->require(is_string($value) && isset($tokens[$key]['values'][$value]), 'Invalid responsive design token.');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user