Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9726af9254 |
No files matched your search
+42
-1
@@ -1,6 +1,6 @@
|
||||
# Projektstatus
|
||||
|
||||
Stand: 14.09.2026, Fassung **1.5.2**. Das System heißt **KantSites** (vorher Arbeitstitel „Kant CMS“). **Startseite von 18 auf 8 Datenbankabfragen, gemessenes Ladebudget, alle langen Listen blättern. Sicherung und Wiederherstellung, letztere nachgewiesen. Globale Seitenbreite wirkt jetzt auch nach oben, vier Menüplätze mit drei Menüarten, und jede Seite wählt ihren Kopf- und Fußbereich – auch „keiner“. 93 Gestaltungseigenschaften, neun Einblendungen, sieben Hover-Effekte, Symbol, Fortschritt und ein Slider ohne JavaScript. Update über Git mit Vorprüfung, Sicherung und Rückweg. Statistik ohne Wiedererkennung und damit ohne Einwilligung. Einwilligung mit tatsächlich zurückgehaltenen Einbettungen, Datenschutz und Impressum als Pflichtseiten. Benutzerverwaltung in der Oberfläche, Mediathek als eigene Seite mit Blättern. Die Verwaltung hat einen gemeinsamen Rahmen mit einer Navigation, die zeigt, wo man ist. Eigenes CSS je Element für alle, eigenes Skript nur für Administratoren und nur als eigene Datei. Formatierter Text mit Werkzeugleiste und HTML-Ansicht, serverseitig gegen eine Positivliste gefiltert. Kopf- und Fußbereich sind frei gestaltbare Blockdokumente, dazu verknüpfte wiederverwendbare Sektionen (Arbeitspaket G, Teil 1). Frei wählbare Startseite; `/sitemap.xml` und Geschwister waren lokal nie erreichbar und sind es jetzt. Seiten verschieben mit automatischen Weiterleitungen, Papierkorb, Fassungsverlauf und geplante Veröffentlichung (Arbeitspaket F). Browsertests wiederhergestellt und erweitert; dabei drei echte Fehler gefunden und behoben, darunter die in der Vorschau wirkungslose Gestaltung. Direktbearbeitung in der Vorschau, Kontrastwarnung, Gestaltungsvorlagen und Bildauswahl für Hintergrundbilder. Der Editor gestaltet frei statt über feste Auswahllisten: eigene Farbpalette, Schriften, und jede Eigenschaft je Block, je Gerät und für den Hover-Zustand frei einstellbar (Dokumentschema v10). SEO-Grundlage mit Sitemap, robots.txt, Weiterleitungen und strukturierten Daten (Arbeitspaket E). Formulare mit serverseitiger Validierung, Einwilligung, E-Mail-Versand und Spamschutz (Arbeitspaket D). Tabs als fortschreitende Verbesserung mit JavaScript (Dokumentschema v7). Zehn neue Blöcke für die Abschnitte der Pilotstartseite (Arbeitspaket C). Beiträge mit Datum, Autor, Auszug, Beitragsbild, Kategorien, Schlagwörtern, Archiven, einbettbarer Beitragsliste und RSS (Arbeitspaket B). Menüs mit Seiten-, Anker- und externen Zielen ersetzen die fest verdrahtete Navigation (Arbeitspaket A). Seiten und Beiträge lassen sich im Browser anlegen und löschen, mit Adresshierarchie bis vier Ebenen.** Die vollständige Phase-1-Abnahme bleibt wegen des fehlenden UX-Builder-Referenzabgleichs offen.
|
||||
Stand: 14.09.2026, Fassung **1.5.3**. Das System heißt **KantSites** (vorher Arbeitstitel „Kant CMS“). **Startseite von 18 auf 8 Datenbankabfragen, gemessenes Ladebudget, alle langen Listen blättern. Sicherung und Wiederherstellung, letztere nachgewiesen. Globale Seitenbreite wirkt jetzt auch nach oben, vier Menüplätze mit drei Menüarten, und jede Seite wählt ihren Kopf- und Fußbereich – auch „keiner“. 93 Gestaltungseigenschaften, neun Einblendungen, sieben Hover-Effekte, Symbol, Fortschritt und ein Slider ohne JavaScript. Update über Git mit Vorprüfung, Sicherung und Rückweg. Statistik ohne Wiedererkennung und damit ohne Einwilligung. Einwilligung mit tatsächlich zurückgehaltenen Einbettungen, Datenschutz und Impressum als Pflichtseiten. Benutzerverwaltung in der Oberfläche, Mediathek als eigene Seite mit Blättern. Die Verwaltung hat einen gemeinsamen Rahmen mit einer Navigation, die zeigt, wo man ist. Eigenes CSS je Element für alle, eigenes Skript nur für Administratoren und nur als eigene Datei. Formatierter Text mit Werkzeugleiste und HTML-Ansicht, serverseitig gegen eine Positivliste gefiltert. Kopf- und Fußbereich sind frei gestaltbare Blockdokumente, dazu verknüpfte wiederverwendbare Sektionen (Arbeitspaket G, Teil 1). Frei wählbare Startseite; `/sitemap.xml` und Geschwister waren lokal nie erreichbar und sind es jetzt. Seiten verschieben mit automatischen Weiterleitungen, Papierkorb, Fassungsverlauf und geplante Veröffentlichung (Arbeitspaket F). Browsertests wiederhergestellt und erweitert; dabei drei echte Fehler gefunden und behoben, darunter die in der Vorschau wirkungslose Gestaltung. Direktbearbeitung in der Vorschau, Kontrastwarnung, Gestaltungsvorlagen und Bildauswahl für Hintergrundbilder. Der Editor gestaltet frei statt über feste Auswahllisten: eigene Farbpalette, Schriften, und jede Eigenschaft je Block, je Gerät und für den Hover-Zustand frei einstellbar (Dokumentschema v10). SEO-Grundlage mit Sitemap, robots.txt, Weiterleitungen und strukturierten Daten (Arbeitspaket E). Formulare mit serverseitiger Validierung, Einwilligung, E-Mail-Versand und Spamschutz (Arbeitspaket D). Tabs als fortschreitende Verbesserung mit JavaScript (Dokumentschema v7). Zehn neue Blöcke für die Abschnitte der Pilotstartseite (Arbeitspaket C). Beiträge mit Datum, Autor, Auszug, Beitragsbild, Kategorien, Schlagwörtern, Archiven, einbettbarer Beitragsliste und RSS (Arbeitspaket B). Menüs mit Seiten-, Anker- und externen Zielen ersetzen die fest verdrahtete Navigation (Arbeitspaket A). Seiten und Beiträge lassen sich im Browser anlegen und löschen, mit Adresshierarchie bis vier Ebenen.** Die vollständige Phase-1-Abnahme bleibt wegen des fehlenden UX-Builder-Referenzabgleichs offen.
|
||||
|
||||
Erster Commit angelegt (`82b0782`); das Projekt hat damit einen Rückfallpunkt. Noch kein Remote und kein Push.
|
||||
|
||||
@@ -369,6 +369,47 @@ als Platzhaltertext, nicht als eigene Kennzeichnung. Noch offen aus Teil 3: Vors
|
||||
Suche im Werkzeugkasten, Vorlagen je Element mit Bild, und das Ziehen der Spaltenbreite in der
|
||||
Vorschau.
|
||||
|
||||
## 1.5.3: der Editor blieb nach dem Update schwarz
|
||||
|
||||
**Der Fehler.** Nach 1.5.2 meldete der Editor im Browser
|
||||
`The requested module './editor-model.js' does not provide an export named 'dropList'`, und der
|
||||
Werkzeugkasten war leer.
|
||||
|
||||
Ursache war nicht der Code, sondern das Zwischenspeichern. `public/.htaccess` setzt für **jede**
|
||||
`.js`-Datei `Cache-Control: public, max-age=31536000, immutable` – ein Jahr. Die Fassungskennung
|
||||
`?v=` hängt aber nur an den Einstiegsdateien, die die Vorlage schreibt. Die fünf Module, die der
|
||||
Editor über `import './….js'` lädt, tragen keine: der Browser holte das neue `editor.js` und
|
||||
behielt das alte `editor-model.js`. Ein neues `canvas-overlay.js` traf damit auf ein Modell ohne
|
||||
`dropList`.
|
||||
|
||||
Beide gemeldeten Symptome hatten diese eine Wurzel: seit 1.5.1 baut das Skript den Werkzeugkasten
|
||||
auf (`buildLibrary()`), und `editor.js` importiert `canvas-overlay.js` in Zeile 5. Bricht der
|
||||
Import, läuft keine Zeile danach – und die Elementliste bleibt leer. Das gemeldete „Menü fehlt“
|
||||
war der Werkzeugkasten, nicht die Verwaltungsnavigation. Die wurde geprüft: in Chrome 152 stehen
|
||||
`::details-content`, ausgeblendeter Schalter und sichtbare Liste korrekt.
|
||||
|
||||
**Die Behebung.** Eine Importkarte in `base.html.twig` hängt die Fassung an die aufgelöste
|
||||
Adresse jedes Moduls, ohne dass eine einzige Importzeile sie kennen muss. Das behält das
|
||||
Zwischenspeichern über ein Jahr – die Leistung war nie das Problem, nur die fehlende Kennung.
|
||||
|
||||
Eine Importkarte muss eingebettet sein; die Spezifikation kennt keine externe. Unter
|
||||
`script-src 'self'` wurde sie wortlos verworfen. `AdminHeadersSubscriber` erzeugt deshalb je
|
||||
Verwaltungsseite einen Nonce, nennt ihn in der Richtlinie und legt ihn in die Anfrage; die
|
||||
Twig-Funktion `csp_nonce()` gibt ihn der Karte mit. Der Nonce erlaubt genau dieses eine Skript –
|
||||
`'self'` gilt unverändert für alles andere.
|
||||
|
||||
**Geprüft.** Der Browsertest schreibt jede angeforderte `.js`-Adresse mit und verlangt an jeder
|
||||
eine Fassungskennung; vor der Behebung nennt er die fünf fehlenden beim Namen, danach trägt sie
|
||||
alle acht. Dazu ein Test, der jeden `./….js`-Import in `public/assets` gegen die Importkarte
|
||||
hält – ein neues Modul, das dort fehlt, fällt damit im Test auf und nicht erst beim nächsten
|
||||
Update beim Kunden. Gegenprobe gemacht: mit einem entfernten Eintrag schlägt er fehl.
|
||||
`npm test` 28 von 28, `php vendor/bin/phpunit` 452 von 452, `npm run test:editor` vollständig.
|
||||
|
||||
**Bekannte Grenze.** Ein Modulfehler legt den Editor weiterhin vollständig lahm, und der
|
||||
Werkzeugkasten ist seit 1.5.1 nichts ohne sein Skript. Für einen Editor ist das vertretbar – die
|
||||
öffentliche Website läuft ohne JavaScript –, aber die Fehleranzeige dafür ist noch die
|
||||
Browserkonsole und kein Satz in der Oberfläche.
|
||||
|
||||
## Nächster konkreter Schritt
|
||||
|
||||
Arbeitspaket H, Teil 3b: Vorschaubilder und Suche im Werkzeugkasten, Vorlagen je Element mit
|
||||
|
||||
@@ -39,6 +39,14 @@ try {
|
||||
page.on('pageerror', error => { errors.push(error.message); console.error('Editor script error:', error.message); });
|
||||
page.on('console', message => { if (message.type() === 'error' && /Content Security Policy/.test(message.text())) errors.push(message.text()); });
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
// Jede angeforderte Skriptdatei mitschreiben. `public/.htaccess` laesst .js ein Jahr lang als
|
||||
// `immutable` zwischenspeichern; eine Datei ohne Fassungskennung waere damit nach einem Update
|
||||
// fuer immer veraltet - genau so brach der Editor mit "does not provide an export named ...".
|
||||
const skripte = [];
|
||||
page.on('request', anfrage => {
|
||||
const url = new URL(anfrage.url());
|
||||
if (url.pathname.startsWith('/assets/') && url.pathname.endsWith('.js')) skripte.push(url);
|
||||
});
|
||||
const axe = async name => {
|
||||
console.log('Accessibility check:', name);
|
||||
// The no-script preview sandbox cannot host axe's frame worker. Audit the identical
|
||||
@@ -61,6 +69,13 @@ try {
|
||||
await page.locator('a[href="/admin/pages/test-home"]').first().click();
|
||||
await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor();
|
||||
console.log('Editor data loaded.');
|
||||
// Der Editor laedt fuenf Module ueber Importe, die selbst keine Fassung tragen; die
|
||||
// Importkarte in base.html.twig haengt sie an. Ohne sie kaeme das alte Modul aus dem Cache.
|
||||
assert.ok(skripte.some(url => url.pathname === '/assets/editor-model.js'), 'editor-model.js wird geladen');
|
||||
const ohneFassung = [...new Set(skripte.filter(url => !url.searchParams.has('v')).map(url => url.pathname))];
|
||||
assert.deepEqual(ohneFassung, [], `Skripte ohne Fassungskennung: ${ohneFassung.join(', ')}`);
|
||||
checks.push({ name: 'every module is requested with a version key', passed: true });
|
||||
console.log(`Fassungskennung an allen ${new Set(skripte.map(u => u.pathname)).size} Skriptdateien.`);
|
||||
console.log('Preview status:', await page.locator('#preview-status').innerText());
|
||||
const frame = page.frameLocator('#preview');
|
||||
await frame.locator('h1').waitFor();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
@@ -14,12 +15,34 @@ final class AdminHeadersSubscriber implements EventSubscriberInterface
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [KernelEvents::RESPONSE => ['onResponse', -300]];
|
||||
return [KernelEvents::REQUEST => ['onRequest', 300], KernelEvents::RESPONSE => ['onResponse', -300]];
|
||||
}
|
||||
|
||||
private function isAdmin(string $path): bool
|
||||
{
|
||||
return 1 === preg_match('#^/'.preg_quote($this->adminPath, '#').'(?:/|$)#', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* One value per administration page, for the single inline script there is: the import map.
|
||||
*
|
||||
* It has to be inline - the specification knows no external import maps - and it has to work
|
||||
* under `script-src 'self'`. Without it the map is dropped without a word, the modules load
|
||||
* without a version key, and a year-old `editor-model.js` out of the cache meets a new
|
||||
* `editor.js`. That is not a theory: it broke the editor after 1.5.0 went out.
|
||||
*/
|
||||
public function onRequest(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
if (!$event->isMainRequest() || !$this->isAdmin($request->getPathInfo())) {
|
||||
return;
|
||||
}
|
||||
$request->attributes->set('csp_nonce', rtrim(strtr(base64_encode(random_bytes(16)), '+/', '-_'), '='));
|
||||
}
|
||||
|
||||
public function onResponse(ResponseEvent $event): void
|
||||
{
|
||||
if (!preg_match('#^/'.preg_quote($this->adminPath, '#').'(?:/|$)#', $event->getRequest()->getPathInfo())) {
|
||||
if (!$this->isAdmin($event->getRequest()->getPathInfo())) {
|
||||
return;
|
||||
}
|
||||
$headers = $event->getResponse()->headers;
|
||||
@@ -30,6 +53,10 @@ final class AdminHeadersSubscriber implements EventSubscriberInterface
|
||||
$headers->set('Permissions-Policy', 'accelerometer=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()');
|
||||
// The preview must show the same typeface as the published page, so the font hosts are
|
||||
// allowed here as well. Everything else stays as narrow as on the public side.
|
||||
$headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'");
|
||||
// The nonce only ever adds the one script it names; 'self' keeps applying to everything
|
||||
// else, so nothing is loosened by it.
|
||||
$nonce = $event->getRequest()->attributes->get('csp_nonce');
|
||||
$script = is_string($nonce) && '' !== $nonce ? sprintf("'self' 'nonce-%s'", $nonce) : "'self'";
|
||||
$headers->set('Content-Security-Policy', "default-src 'self'; script-src {$script}; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* The nonce for the one inline script the administration needs.
|
||||
*
|
||||
* `script-src 'self'` has no room for an inline script, and the import map has to be one: the
|
||||
* specification knows no external import maps. AdminHeadersSubscriber puts a fresh value into the
|
||||
* request for every administration page and names it in the policy; this hands the same value to
|
||||
* the template. Outside the administration there is none, and the function then returns an empty
|
||||
* string instead of inventing one that no policy allows.
|
||||
*/
|
||||
final class CspExtension extends AbstractExtension
|
||||
{
|
||||
public function __construct(private RequestStack $requests)
|
||||
{
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [new TwigFunction('csp_nonce', $this->nonce(...))];
|
||||
}
|
||||
|
||||
public function nonce(): string
|
||||
{
|
||||
$value = $this->requests->getCurrentRequest()?->attributes->get('csp_nonce');
|
||||
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,20 @@
|
||||
<title>{% block title %}Verwaltung{% endblock %} · KantSites</title>
|
||||
{% include 'favicon.html.twig' %}
|
||||
<link rel="stylesheet" href="/assets/admin.css?v={{ kant_version() }}">
|
||||
{# Die Fassungskennung fuer die Module untereinander.
|
||||
|
||||
Die Einstiegsdatei bekommt ihr `?v=` aus der Vorlage, ein `import './editor-model.js'`
|
||||
darin nicht - und `public/.htaccess` laesst jede .js-Datei ein Jahr lang als `immutable`
|
||||
zwischenspeichern. Nach einem Update lud der Browser damit das neue `editor.js`, behielt
|
||||
aber das alte `editor-model.js` und brach mit "does not provide an export named ..." ab.
|
||||
Die Importkarte haengt die Fassung an die aufgeloeste Adresse, ohne dass eine einzige
|
||||
Importzeile sie kennen muss. Sie steht vor jedem Modul-Skript; spaeter geladen wirkt sie
|
||||
nicht mehr. #}
|
||||
<script type="importmap" nonce="{{ csp_nonce() }}">{"imports":{
|
||||
{%- for modul in ['editor-model', 'media', 'design-panel', 'rich-text', 'canvas-overlay'] %}
|
||||
"/assets/{{ modul }}.js": "/assets/{{ modul }}.js?v={{ kant_version() }}"{{ loop.last ? '' : ',' }}
|
||||
{%- endfor %}
|
||||
}}</script>
|
||||
<script src="/assets/color-field.js?v={{ kant_version() }}" defer></script>
|
||||
{% block styles %}{% endblock %}
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { EditorHistory, createBlock, locate, insertBlock, insertBlockAt, moveBlockTo, dropList, insertionTarget, insertRowAt, CATEGORIES, labels as blockLabels, duplicateBlock, moveBlock, upgradeDocument, effectiveDesign, setDesign, insertPreset, insertRow, blockPath, insertMedia, setEffect, isPluginBlock, blockLabel, blockIcon, canContain, isContainer, setStyle, effectiveStyle, hoverStyle, resetStyleDevice, clearStyle, copyStyle, SCHEMA_VERSION } from '../public/assets/editor-model.js';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { designCss } from '../scripts/build-design.mjs';
|
||||
@@ -492,3 +492,27 @@ test('a row dropped at an exact position brings its columns along', () => {
|
||||
// Und in eine Zeile hinein gibt es keine zweite Zeile.
|
||||
assert.equal(insertRowAt(document, 2, row.id, 0), null);
|
||||
});
|
||||
|
||||
// --- Fassungskennung an den Modulen ---------------------------------------------------------
|
||||
|
||||
test('every module imported by another one is listed in the import map', () => {
|
||||
// `public/.htaccess` laesst jede .js-Datei ein Jahr lang als `immutable` zwischenspeichern.
|
||||
// Die Einstiegsdatei bekommt ihr `?v=` aus der Vorlage, ein `import './editor-model.js'` darin
|
||||
// nicht - der Browser behielte das alte Modul und der Editor braeche mit "does not provide an
|
||||
// export named ..." ab. Genau das ist nach 1.5.0 passiert. Die Importkarte haengt die Fassung
|
||||
// an; ein neues Modul, das dort fehlt, faellt hier auf und nicht erst beim naechsten Update.
|
||||
const quellen = readdirSync('public/assets').filter(name => name.endsWith('.js'));
|
||||
const importiert = new Set();
|
||||
for (const name of quellen) {
|
||||
for (const treffer of readFileSync(`public/assets/${name}`, 'utf8').matchAll(/from '\.\/([\w-]+\.js)'/g)) {
|
||||
importiert.add(treffer[1]);
|
||||
}
|
||||
}
|
||||
assert.ok(importiert.size > 0, 'es gibt ueberhaupt Modulimporte');
|
||||
const vorlage = readFileSync('templates/admin/base.html.twig', 'utf8');
|
||||
const karte = /<script type="importmap"[\s\S]*?<\/script>/.exec(vorlage);
|
||||
assert.ok(karte, 'base.html.twig traegt eine Importkarte');
|
||||
const eingetragen = new Set([...karte[0].matchAll(/'([\w-]+)'/g)].map(treffer => `${treffer[1]}.js`));
|
||||
const fehlend = [...importiert].filter(name => !eingetragen.has(name)).sort();
|
||||
assert.deepEqual(fehlend, [], `ohne Fassungskennung in der Importkarte: ${fehlend.join(', ')}`);
|
||||
});
|
||||
Reference in new issue
Block a user