Redirect the other www spelling to SITE_ORIGIN, hold roles to the role help and fix the audit findings

SITE_ORIGIN alone decides whether the site is www or not: CanonicalHostSubscriber
sends the other spelling there with 301, cms:site:origin writes the setting, the
system screen shows it. The roles now match the role help (editor: drafts,
publisher: live content, admin: design, settings, users, system). The installer no
longer reopens on an unreachable database, the session cookie follows ADMIN_PATH,
percent-encoded credentials no longer break the container, consent only accepts
same-site decisions, JSON-LD is HTML-safe, own CSS stays in its block, plus the
editor, updater, download and public-output findings from the full review
(ADR 099, ADR 100).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.1 committed 2026-09-15 20:15:20 +02:00
1 parent 45040b2ca6
commit 7eb462ac7b
53 files changed
+1023 -122

No files matched your search

+4
View File
@@ -53,6 +53,7 @@
</section>
<section class="stack-section">
<div class="section-head"><h2>Unternehmensdaten</h2><span class="muted">Grundlage für Organization und LocalBusiness</span></div>
{% if is_granted('ROLE_ADMIN') %}
<form action="{{ path('admin_settings_save') }}" method="post" class="card card-body">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
<div class="fields">
@@ -273,6 +274,9 @@
<button class="primary" type="submit">Einstellungen speichern</button>
</form>
{% else %}
<p class="card card-body muted">Unternehmensdaten, Knopf „nach oben“, Spamschutz und Einwilligung ändert nur ein Administrator.</p>
{% endif %}
</section>
<section class="stack-section">
+7 -7
View File
@@ -32,20 +32,20 @@
{% set entries = [
{id: 'pages', route: 'admin_pages', icon: 'pages', label: 'Seiten'},
{id: 'posts', route: 'admin_posts', icon: 'pages', label: 'Beiträge'},
{id: 'partials', route: 'admin_partials', icon: 'partials', label: 'Bereiche'},
{id: 'menus', route: 'admin_menus', icon: 'menus', label: 'Menüs'},
{id: 'partials', route: 'admin_partials', icon: 'partials', label: 'Bereiche', role: 'ROLE_PUBLISHER'},
{id: 'menus', route: 'admin_menus', icon: 'menus', label: 'Menüs', role: 'ROLE_PUBLISHER'},
{id: 'media', route: 'admin_media', icon: 'media', label: 'Mediathek'},
{id: 'forms', route: 'admin_forms', icon: 'forms', label: 'Formulare'},
{id: 'forms', route: 'admin_forms', icon: 'forms', label: 'Formulare', role: 'ROLE_PUBLISHER'},
{id: 'statistics', route: 'admin_statistics', icon: 'statistics', label: 'Statistik'}
] %}
{% for entry in entries %}
{% for entry in entries %}{% if entry.role is not defined or is_granted(entry.role) %}
<li><a class="nav-item" href="{{ path(entry.route) }}"{{ nav|default('') == entry.id ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: entry.icon} only %}{{ entry.label }}</a></li>
{% endfor %}
{% endif %}{% endfor %}
</ul>
<p class="nav-label" id="nav-site">WEBSITE</p>
<ul class="nav-list" aria-labelledby="nav-site">
<li><a class="nav-item" href="{{ path('admin_theme') }}"{{ nav|default('') == 'theme' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'theme'} only %}Design</a></li>
<li><a class="nav-item" href="{{ path('admin_settings') }}"{{ nav|default('') == 'settings' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'settings'} only %}Einstellungen</a></li>
{% if is_granted('ROLE_ADMIN') %}<li><a class="nav-item" href="{{ path('admin_theme') }}"{{ nav|default('') == 'theme' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'theme'} only %}Design</a></li>{% endif %}
{% if is_granted('ROLE_PUBLISHER') %}<li><a class="nav-item" href="{{ path('admin_settings') }}"{{ nav|default('') == 'settings' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'settings'} only %}Einstellungen</a></li>{% endif %}
{% if is_granted('ROLE_ADMIN') %}<li><a class="nav-item" href="{{ path('admin_system') }}"{{ nav|default('') == 'system' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'system'} only %}System</a></li>{% endif %}
{% if is_granted('ROLE_ADMIN') %}<li><a class="nav-item" href="{{ path('admin_tasks') }}"{{ nav|default('') == 'tasks' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'tasks'} only %}Aufgaben</a></li>{% endif %}
{% if is_granted('ROLE_ADMIN') %}<li><a class="nav-item" href="{{ path('admin_users') }}"{{ nav|default('') == 'users' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'users'} only %}Benutzer</a></li>{% endif %}
+10
View File
@@ -34,6 +34,16 @@
{% endif %}
<br><span class="muted">Eingestellt in <code>{{ database.quelle }}</code>; Server-Umgebungsvariablen haben Vorrang.</span>
</dd>
<dt>Adresse</dt>
<dd>
{% if origin.adresse %}<code>{{ origin.adresse }}</code>{% else %}<span class="check-bad"><strong>Nicht eingetragen.</strong></span>{% endif %}
{% if origin.passt %}
<span class="muted">– Canonical, Sitemap und Feed nennen sie; die Schreibweise {{ origin.adresse matches '{^https?://www\.}' ? 'ohne' : 'mit' }} www leitet hierher um.</span>
{% else %}
<br><span class="check-bad"><strong>Diese Verwaltung läuft gerade unter <code>{{ origin.host }}</code>.</strong> Canonical, Sitemap und Feed nennen trotzdem die eingetragene Adresse. Stimmt die nicht mehr: <code>php bin/console cms:site:origin https://…</code> oder <code>SITE_ORIGIN</code> in <code>.env</code> ändern.</span>
{% endif %}
<br><span class="muted">Eingestellt in <code>{{ origin.quelle }}</code>; Server-Umgebungsvariablen haben Vorrang.</span>
</dd>
{% if state.worktree %}
<dt>Stand</dt><dd><code>{{ state.describe }}</code>{% if state.commit != 'unbekannt' %} · Commit <code>{{ state.commit }}</code>{% endif %}</dd>
{% else %}
+2 -2
View File
@@ -27,7 +27,7 @@
<a class="skip-link" href="#main">Zum Inhalt springen</a>
{% if areas.header|default(null) %}
<header class="site-header{{ siteSettings.load().stickyHeader ? ' is-sticky' : '' }}{{ siteSettings.load().headerOverlay ? ' is-overlay' : '' }}{{ kant_header_surface(areas.header) ? ' has-own-surface' : '' }}">
{% for block in areas.header.blocks %}{% include 'blocks/block.html.twig' with {block: block, postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: true, richHeadings: areas.header.schemaVersion|default(1) >= 17, eagerImage: null} only %}{% endfor %}
{% for block in areas.header.blocks %}{% include 'blocks/block.html.twig' with {block: block, postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: areas.header.schemaVersion|default(1) >= 12, richHeadings: areas.header.schemaVersion|default(1) >= 17, eagerImage: null} only %}{% endfor %}
</header>
{% endif %}
<main id="main" tabindex="-1">
@@ -53,7 +53,7 @@
</main>
{% if areas.footer|default(null) %}
<footer class="site-footer">
{% for block in areas.footer.blocks %}{% include 'blocks/block.html.twig' with {block: block, postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: true, richHeadings: areas.footer.schemaVersion|default(1) >= 17, eagerImage: null} only %}{% endfor %}
{% for block in areas.footer.blocks %}{% include 'blocks/block.html.twig' with {block: block, postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: areas.footer.schemaVersion|default(1) >= 12, richHeadings: areas.footer.schemaVersion|default(1) >= 17, eagerImage: null} only %}{% endfor %}
</footer>
{% endif %}
{% if not editing|default(false) and consentGate.needsBanner(app.request) %}
+1 -1
View File
@@ -220,7 +220,7 @@
frames, and a preview must not load anything from outside either. #}
{% if not editing|default(false) and consentGate.granted(app.request, props.group) %}
<div id="{{ block.id }}" class="block-embed{{ designClasses }}">
<iframe src="{{ props.url }}" title="{{ props.title }}" height="{{ props.height }}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin"></iframe>
<iframe src="{{ props.url }}" title="{{ props.title }}" height="{{ props.height }}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" allow="fullscreen; picture-in-picture"></iframe>
</div>
{% else %}
<div id="{{ block.id }}" class="block-embed embed-blocked{{ designClasses }}">
+1 -1
View File
@@ -11,7 +11,7 @@
<title>{{ post.title }}</title>
<link>{{ origin }}{{ post.href }}</link>
<guid isPermaLink="true">{{ origin }}{{ post.href }}</guid>
{% if post.publishedAt %}<pubDate>{{ post.publishedAt|date('r') }}</pubDate>{% endif %}
{% if post.publishedAt %}<pubDate>{{ (post.publishedAt ~ ' UTC')|date('r', 'UTC') }}</pubDate>{% endif %}
{% if post.excerpt %}<description>{{ post.excerpt }}</description>{% endif %}
{% for term in post.terms.category %}<category>{{ term.name }}</category>{% endfor %}
</item>
+1 -1
View File
@@ -60,7 +60,7 @@
show the layout, not run code that could change the page under the editor's hands. #}
{% if pageScript|default(null) and not editing|default(false) %}<script src="{{ pageScript }}" defer></script>{% endif %}
{# A data block, not a script: CSP script-src does not apply to application/ld+json. #}
{% for block in structuredData|default([]) %}<script type="application/ld+json">{{ block|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_UNESCAPED_UNICODE'))|raw }}</script>
{% for block in structuredData|default([]) %}<script type="application/ld+json">{{ block|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_UNESCAPED_UNICODE') b-or constant('JSON_HEX_TAG') b-or constant('JSON_HEX_AMP'))|raw }}</script>
{% endfor %}
{% if pageStyles|default('') %}
{# In the editor the preview inherits the administration's policy, which forbids inline