Keep old pages readable, detect stale settings writes, make backups honest

- Own CSS is parsed on write only. A page whose stored CSS no longer
  passes a stricter rule answers 200 and opens in the editor; that one
  block loses its CSS. A document read several times in one request is
  validated once.
- Settings and theme refuse a write from an outdated form.
- A backup fails when a media file does not arrive completely and
  leaves no half directory behind; rotation counts intact backups only.
- The archive update takes its backup behind the maintenance page.
- A package import puts all media in place before removing the old
  ones, reports honestly when that fails and keeps a maintenance state
  that was already on.
- Restoring replaces the SQLite file in one move and points at pending
  migrations.
- Six irreversible actions ask first, in a dialog of the admin's own.
- Ctrl+S in the editor takes the field being typed in along; Delete
  no longer removes a block while a dialog is open.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.2 committed 2026-11-10 11:41:05 +01:00
1 parent 0d61aa0f22
commit ead47cc924
25 files changed
+437 -72

No files matched your search

+1
View File
@@ -23,6 +23,7 @@
{%- endfor %}
}}</script>
<script src="/assets/color-field.js?v={{ kant_version() }}" defer></script>
<script src="/assets/confirm.js?v={{ kant_version() }}" defer></script>
{% block styles %}{% endblock %}
{% block scripts %}{% endblock %}
{# Eigener Block, nicht Teil von `scripts`: Bildschirme wie die Mediathek ersetzen `scripts`
+1 -1
View File
@@ -170,7 +170,7 @@
{# The class is what the security test looks for: an editor must not even be
offered this. #}
<div class="delete-panel">
<form action="{{ path('admin_delete', {id: page.id}) }}" method="post">
<form action="{{ path('admin_delete', {id: page.id}) }}" method="post" data-confirm-title="„{{ page.title }}“ endgültig löschen?" data-confirm-text="Die Seite wird mit allen Fassungen gelöscht. Das lässt sich nicht rückgängig machen." data-confirm-label="Endgültig löschen" data-confirm-variant="danger">
<input type="hidden" name="_list_type" value="{{ contentType }}"><input type="hidden" name="_list_query" value="{{ query }}"><input type="hidden" name="_list_status" value="{{ status }}"><input type="hidden" name="_list_page" value="{{ result.page }}"><input type="hidden" name="_list_limit" value="{{ result.limit }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('pages') }}">
<input type="hidden" name="expectedVersion" value="{{ page.version }}">
+1 -1
View File
@@ -123,7 +123,7 @@
{% if mail.protokoll is empty %}
<span class="muted">var/log/mail.log</span>
{% else %}
<form action="{{ path('admin_mail_clear') }}" method="post">
<form action="{{ path('admin_mail_clear') }}" method="post" data-confirm-title="Versandprotokoll leeren?" data-confirm-text="Alle Einträge in var/log/mail.log werden entfernt." data-confirm-label="Protokoll leeren" data-confirm-variant="danger">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
<button class="text-button" type="submit">Protokoll leeren</button>
</form>
+1 -1
View File
@@ -57,7 +57,7 @@
<div class="sheet-part">
<h4>Entfernen</h4>
<p>Entfernt „{{ partial.name }}“.{% if partial.usedBy is not empty %} <strong>Noch eingebunden in {{ partial.usedBy|join(', ') }}</strong> – dort erscheint danach nichts mehr.{% endif %}</p>
<form action="{{ path('admin_partial_delete', {id: partial.id}) }}" method="post">
<form action="{{ path('admin_partial_delete', {id: partial.id}) }}" method="post" data-confirm-title="Sektion „{{ partial.name }}“ entfernen?" data-confirm-text="{% if partial.usedBy is not empty %}Sie ist noch eingebunden in {{ partial.usedBy|join(', ') }} – dort erscheint danach nichts mehr. {% endif %}Das lässt sich nicht rückgängig machen." data-confirm-label="Sektion entfernen" data-confirm-variant="danger">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('partials') }}">
<button class="subtle danger-text" type="submit">Sektion entfernen</button>
</form>
+2 -1
View File
@@ -104,6 +104,7 @@
{% 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') }}">
<input type="hidden" name="expected" value="{{ settingsStamp }}">
<div class="fields">
<div>
<label for="legalName">Name des Unternehmens</label>
@@ -390,7 +391,7 @@
<td>{% if entry.to %}<code>/{{ entry.to }}</code>{% else %}<span class="muted">—</span>{% endif %}</td>
<td>{{ entry.status }}</td>
<td>
<form action="{{ path('admin_redirect_save') }}" method="post">
<form action="{{ path('admin_redirect_save') }}" method="post" data-confirm-title="Weiterleitung von /{{ entry.from }} entfernen?" data-confirm-text="Die alte Adresse antwortet danach mit „nicht gefunden“." data-confirm-label="Entfernen" data-confirm-variant="danger">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
<input type="hidden" name="remove" value="{{ entry.id }}">
<button class="text-button danger-text" type="submit" aria-label="Weiterleitung von /{{ entry.from }} entfernen">Entfernen</button>
+1
View File
@@ -82,6 +82,7 @@
{% set emptyFont = {key: '', name: '', family: '', source: 'google', fallback: 'sans-serif', weights: [400, 700]} %}
<form action="{{ path('admin_theme_save') }}" method="post" class="theme-form stack" data-theme-editor data-palettes="{{ palettes|json_encode|e('html_attr') }}" data-color-names="{{ colorNames|json_encode|e('html_attr') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('theme') }}">
<input type="hidden" name="expected" value="{{ themeStamp }}">
<section class="card">
<div class="card-head">
+1 -1
View File
@@ -87,7 +87,7 @@
<div class="sheet-part">
<h4>Konto löschen</h4>
<p>Entfernt „{{ account.username }}“. Gespeicherte Inhalte bleiben unberührt – Seiten und Fassungen hängen nicht am Konto. Das eigene Konto lässt sich nicht löschen; deshalb bleibt immer mindestens ein Administrator übrig.</p>
<form action="{{ path('admin_user_delete', {id: account.id}) }}" method="post">
<form action="{{ path('admin_user_delete', {id: account.id}) }}" method="post" data-confirm-title="Konto „{{ account.username }}“ löschen?" data-confirm-text="Die Person kann sich danach nicht mehr anmelden. Seiten und Fassungen bleiben." data-confirm-label="Konto löschen" data-confirm-variant="danger">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('users') }}">
<button class="subtle danger-text" type="submit">Konto löschen</button>
</form>