Compare commits
2
Commits
daaeb52588
...
ef7cb02cbd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef7cb02cbd | ||
|
|
7eaa3f6efa |
No files matched your search
@@ -1,5 +1,15 @@
|
||||
# Projektstatus
|
||||
|
||||
## Nach 1.35.0: E-Mail-Versand und Systemseite aufgeräumt, Protokoll leeren (13.10.2026)
|
||||
|
||||
**Anlass:** Umut: E-Mail-Seite und „Inhalte übertragen“ sehen uneinheitlich aus, Unterpunkt „E-Mail-Versand“ steht immer offen, „Logs leeren“ fehlt.
|
||||
|
||||
**Erledigt:** Fließtext in Karten hat jetzt durchgehend 13 px (vorher Browser-16 px neben 13 px), Felder bleiben 16 px. Neue gemeinsame Bausteine in `admin.css`: `.card-section` (Abschnitt mit Trennlinie), `.button-row`, `.form-foot`, `.option`/`.option-list` (Auswahl als Karten), `.pill-error`, Dateiauswahl im Knopfstil. E-Mail-Seite: Wege als Karten mit Erklärung, SMTP 2×2, Testmail-Abschnitt, „Speichern“ als Hauptknopf im Kartenfuß (war ungestylt). Protokoll mit Zustandsplakette, Hinweis vorn, Servermeldung zugeklappt (`admin/_mail_entry.html.twig`). „Protokoll leeren“ (`POST /admin/system/e-mail/protokoll`, löscht `mail.log` und `mail.log.1`). Unterpunkt nur im Bereich System sichtbar; in der waagrechten Leiste (761–1000 px) neben „System“.
|
||||
|
||||
**Geprüft:** `SystemScreenTest` erweitert (Leeren nur mit Token, danach leer, Editor 403, Unterpunkt auf Einstellungen nicht sichtbar). PHPUnit vollständig 682 grün, php-cs-fixer sauber, `lint:twig`, `test:admin`, `test:editor` grün. Im Browser (Hüfner lokal) bei 1240, 900 und 375 px: E-Mail-Seite, Systemseite, Einstellungen; kein waagrechtes Scrollen am Handy.
|
||||
|
||||
**Offen:** nicht veröffentlicht (kein Tag/Release).
|
||||
|
||||
## Fassung 1.35.0: E-Mail-Versand als eigene Seite, HTTPS-Probe bei IONOS (12.10.2026)
|
||||
|
||||
**Anlass:** huefner-gartenbau.de: mit `native://default` geht der Versand, Umut will die Einstellung samt SMTP in der Verwaltung, als Unterpunkt statt auf „System“. „HTTPS erzwingen“ meldete 403, obwohl HTTPS lief.
|
||||
|
||||
+69
-14
@@ -293,7 +293,24 @@ label { display: block; margin: 18px 0 7px; font-size: 13px; font-weight: 600 }
|
||||
}
|
||||
.card-head h2, .card-head h3 { margin: 0 }
|
||||
.card-head .muted { font-size: 12px }
|
||||
.card-body { padding: 8px 22px 24px; min-width: 0 }
|
||||
.card-body { padding: 8px 22px 24px; min-width: 0; font-size: 13px; line-height: 1.6 }
|
||||
/* Fließtext in Karten ist 13 px wie Hinweise und Fakten. Felder bleiben bei 16 px - darunter
|
||||
vergrößert iOS beim Antippen die ganze Seite. :where() hält die Regel schwächer als jede Klasse. */
|
||||
:where(.card-body) :where(input:not([type=checkbox], [type=radio]), select, textarea) { font-size: 16px }
|
||||
.card-body h3 { font-size: 14px; margin: 26px 0 4px }
|
||||
.card-body h3:first-child { margin-top: 14px }
|
||||
/* Abschnitte einer Karte: Trennlinie, Überschrift, gleicher Abstand - als fieldset oder form. */
|
||||
.card-section { border: 0; border-top: 1px solid var(--line); margin: 22px 0 0; padding: 18px 0 0; min-width: 0 }
|
||||
.card-section > legend { float: left; width: 100%; font-weight: 650; font-size: 14px; margin: 0 0 12px; padding: 0 }
|
||||
.card-section > legend + * { clear: both }
|
||||
.card-section > h3, .card-body .card-section > h3:first-child { font-size: 14px; font-weight: 650; margin: 0 0 12px }
|
||||
.card-section .check-label { margin: 14px 0 0 }
|
||||
.card-section > h3 + .check-label, .card-section > h3 + input + .check-label { margin-top: 0 }
|
||||
.card-section label.field-first { margin-top: 0 }
|
||||
/* Dateiauswahl im Stil der übrigen Knöpfe statt des grauen Browserknopfs. */
|
||||
input[type=file] { padding: 6px; cursor: pointer; max-width: 720px }
|
||||
input[type=file]::file-selector-button { margin-right: 12px; padding: 7px 14px; border: 1px solid var(--line); border-radius: 6px; background: #f3f5f1; color: var(--ink); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer }
|
||||
input[type=file]:hover::file-selector-button { background: #e8ede6 }
|
||||
.card-body code { overflow-wrap: anywhere }
|
||||
/* Das Kleingedruckte einer Karte: zugeklappt, damit auf einem Handy der Knopf oben bleibt. */
|
||||
.hint { margin: 12px 0 0 }
|
||||
@@ -306,6 +323,22 @@ label { display: block; margin: 18px 0 7px; font-size: 13px; font-weight: 600 }
|
||||
.card-body > p:first-child { margin-top: 14px }
|
||||
.card-body .primary, .card-body .secondary { margin-top: 22px }
|
||||
.card-foot { padding: 16px 22px; border-top: 1px solid var(--line); background: #fafbf8; border-radius: 0 0 12px 12px }
|
||||
/* Der Fuss eines Formulars in einer Karte: Hauptknopf links, Erklärung daneben. */
|
||||
.form-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; font-size: 12px }
|
||||
.form-foot .primary { margin: 0 }
|
||||
/* Knöpfe nebeneinander, gleicher Abstand, ohne den Abstand nach oben der Einzelknöpfe. */
|
||||
.button-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px }
|
||||
.card-body .button-row .primary, .card-body .button-row .secondary { margin: 0 }
|
||||
/* Auswahl als Karten: ganze Fläche klickbar, Titel und eine Zeile Erklärung. */
|
||||
.option-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 8px; max-width: 720px }
|
||||
.option { display: flex; align-items: flex-start; gap: 10px; margin: 0; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--paper); font-weight: 400; cursor: pointer }
|
||||
.option:hover { border-color: #a9b8af }
|
||||
.option:has(input:checked) { border-color: var(--accent); background: #f4f8f1; box-shadow: inset 0 0 0 1px var(--accent) }
|
||||
.option input { width: 16px; min-height: 16px; margin: 2px 0 0; flex: none; accent-color: var(--accent) }
|
||||
.option > span { display: grid; gap: 2px; min-width: 0 }
|
||||
.option strong { font-size: 13px; font-weight: 600 }
|
||||
.option > span > span { font-size: 12px; color: var(--muted); line-height: 1.45 }
|
||||
.check-label input, .option input { accent-color: var(--accent) }
|
||||
|
||||
/* A card that opens: used for everything secondary, so a screen starts calm. */
|
||||
details.card > summary {
|
||||
@@ -376,6 +409,7 @@ details.card > summary:hover { background: #fafbf8 }
|
||||
.pill-live { background: #e7f0d6; color: #2c5340 }
|
||||
.pill-draft { background: #fff2d9; color: #6f4e18 }
|
||||
.pill-off { background: #f1f2f0; color: #5a635e }
|
||||
.pill-error { background: #fff0eb; color: var(--danger) }
|
||||
|
||||
/* The per-row settings. One disclosure that opens in place, instead of six competing popovers
|
||||
that clip each other at the edge of the card. */
|
||||
@@ -593,6 +627,10 @@ details.card > summary:hover { background: #fafbf8 }
|
||||
.nav-label { display: none }
|
||||
.nav-list { display: flex; gap: 4px }
|
||||
.nav-item { white-space: nowrap; padding: 8px 12px }
|
||||
/* In der Leiste stehen Unterpunkte direkt neben ihrem Hauptpunkt, nicht in einer eigenen Zeile. */
|
||||
.nav-list > li:has(.nav-sub) { display: flex; gap: 4px }
|
||||
.nav-sub { display: flex; margin: 0 }
|
||||
.nav-sub .nav-item { padding: 8px 12px }
|
||||
.shell-foot { margin: 0 0 0 auto; padding: 0 0 0 12px; border-top: 0; border-left: 1px solid #2a3d35; display: flex; align-items: center; gap: 10px }
|
||||
.shell-account { padding: 3px; gap: 2px }
|
||||
.shell-user { padding: 3px }
|
||||
@@ -718,23 +756,37 @@ details.card > summary:hover { background: #fafbf8 }
|
||||
.check-bad { color: var(--danger) }
|
||||
.log-list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 6px }
|
||||
.log-list code { font-size: 11px; overflow-wrap: anywhere }
|
||||
.mail-result { margin-top: 14px }
|
||||
#mail .facts { margin-top: 0 }
|
||||
.mail-result { margin-top: 16px }
|
||||
.mail-result p { margin: 0 0 6px }
|
||||
.mail-result p:last-child { margin-bottom: 0 }
|
||||
.mail-result pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 11px; margin: 6px 0 0 }
|
||||
.mail-result-head { margin: 0 0 10px }
|
||||
/* Unterpunkte im Menü: eingerückt auf die Höhe des Textes, ohne Symbol. */
|
||||
.nav-sub { list-style: none; margin: 2px 0 4px; padding: 0 }
|
||||
.nav-sub .nav-item { padding-left: 42px; font-size: 13px }
|
||||
.mail-form { margin-top: 18px; max-width: 640px }
|
||||
.mail-form fieldset { border: 0; margin: 0 0 16px; padding: 0; min-width: 0 }
|
||||
.mail-form legend { font-weight: 600; font-size: 13px; margin-bottom: 6px; padding: 0 }
|
||||
.mail-form .check-label { display: flex; gap: 8px; align-items: baseline; margin: 4px 0 }
|
||||
.mail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px 12px }
|
||||
.mail-grid label { margin-top: 0 }
|
||||
.mail-grid input { width: 100%; box-sizing: border-box }
|
||||
.mail-actions { display: flex; flex-wrap: wrap; gap: 8px }
|
||||
.mail-actions button { margin: 0 }
|
||||
.nav-sub { list-style: none; margin: 2px 0 4px; padding: 0; display: grid; gap: 2px }
|
||||
.nav-sub .nav-item { padding: 7px 10px 7px 39px; font-size: 12.5px; color: var(--shell-muted) }
|
||||
.nav-sub .nav-item:hover { color: var(--shell-ink) }
|
||||
.nav-sub .nav-item[aria-current=page] { color: #fff; background: #21362d }
|
||||
/* E-Mail-Versand: der eingestellte Weg als Zeile, darunter Abschnitte mit Trennlinie. */
|
||||
.mail-current { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-top: 14px; padding: 12px 14px; border-radius: 10px; background: #f6f8f4 }
|
||||
.mail-current-source { flex-basis: 100%; font-size: 12px }
|
||||
.mail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0 14px; max-width: 720px }
|
||||
.mail-grid label { margin: 0 0 7px }
|
||||
.mail-grid > div { margin-bottom: 4px }
|
||||
/* Protokoll: Zustand links, daneben was, wann und - bei Fehlern - warum. */
|
||||
.mail-log { list-style: none; margin: 14px 0 0; padding: 0 }
|
||||
.mail-entry { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 4px 14px; align-items: start; padding: 12px 0; border-top: 1px solid var(--line) }
|
||||
.mail-entry:first-child { border-top: 0; padding-top: 4px }
|
||||
.mail-entry .pill { justify-self: start; margin-top: 1px }
|
||||
.mail-entry-text p { margin: 0 }
|
||||
.mail-entry-line { overflow-wrap: anywhere }
|
||||
.mail-entry-time { font-size: 12px; color: var(--muted) }
|
||||
.mail-entry-hint { margin-top: 6px !important; color: var(--ink) }
|
||||
.mail-entry-detail { margin-top: 6px }
|
||||
.mail-entry-detail summary { font-size: 12px; font-weight: 600; color: var(--muted); cursor: pointer; width: fit-content }
|
||||
.mail-entry-detail pre { margin: 6px 0 0; padding: 10px 12px; border-radius: 8px; background: #f6f8f4; font-size: 11px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere }
|
||||
@media (max-width: 560px) {
|
||||
.mail-entry { grid-template-columns: minmax(0, 1fr) }
|
||||
}
|
||||
/* Ohne SMTP brauchen die Felder niemand; wo :has() fehlt, bleiben sie einfach sichtbar. */
|
||||
.mail-form:has(input[name=weg]:checked:not([value=smtp])) .smtp-fields { display: none }
|
||||
|
||||
@@ -848,6 +900,9 @@ details.card > summary:hover { background: #fafbf8 }
|
||||
.nav-list { display: grid; gap: 2px }
|
||||
/* Vierundvierzig Pixel: die Flaeche, die ein Daumen zuverlaessig trifft. */
|
||||
.nav-item { min-height: 44px; display: flex; align-items: center; gap: 12px; padding: 10px 14px; white-space: normal }
|
||||
.nav-list > li:has(.nav-sub) { display: block }
|
||||
.nav-sub { display: grid; margin: 2px 0 4px }
|
||||
.nav-sub .nav-item { padding: 10px 14px 10px 44px }
|
||||
.shell-foot { margin-left: auto; border-left: 0; padding-left: 0 }
|
||||
.shell-version { display: none }
|
||||
}
|
||||
|
||||
@@ -113,6 +113,19 @@ final class MailController extends AbstractController
|
||||
return $this->backToMail($request);
|
||||
}
|
||||
|
||||
/** „Letzte Versuche“ leeren: var/log/mail.log und mail.log.1. */
|
||||
#[Route('/%kant.admin_path%/system/e-mail/protokoll', name: 'admin_mail_clear', methods: ['POST'])]
|
||||
public function clear(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
if (!$this->isCsrfTokenValid('system', $request->request->getString('_csrf_token'))) {
|
||||
return $this->backToMail($request, 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||||
}
|
||||
$this->mail->clearLog();
|
||||
|
||||
return $this->backToMail($request, 'Das Protokoll ist leer.', 'ok');
|
||||
}
|
||||
|
||||
/** @param array{an: string, von: string} $eingabe */
|
||||
private function mailAdressFehler(array $eingabe): ?string
|
||||
{
|
||||
|
||||
@@ -212,6 +212,16 @@ final class MailCheck
|
||||
return $eintraege;
|
||||
}
|
||||
|
||||
/** Protokoll leeren: die Datei und ihre ältere Hälfte. */
|
||||
public function clearLog(): void
|
||||
{
|
||||
foreach ([$this->logFile, $this->logFile.'.1'] as $datei) {
|
||||
if (is_file($datei)) {
|
||||
@unlink($datei);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function logFile(): string
|
||||
{
|
||||
return $this->logFile;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{# Ein Versuch, gleich gebaut für „Letzte Versuche“ und „Alle Wege ausprobiert“: Zustand, worum es
|
||||
ging, wann - bei Fehlern der Hinweis vorn und die Meldung des Servers zugeklappt dahinter. #}
|
||||
<li class="mail-entry">
|
||||
<span class="pill {{ eintrag.ok ? 'pill-live' : 'pill-error' }}">{{ eintrag.ok ? 'Angenommen' : 'Fehlgeschlagen' }}</span>
|
||||
<div class="mail-entry-text">
|
||||
<p class="mail-entry-line">
|
||||
{% if titel is defined %}<strong>{{ titel }}</strong>{% else %}{{ eintrag.anlass }} an <code>{{ eintrag.an }}</code>{% if eintrag.von %} von <code>{{ eintrag.von }}</code>{% endif %}{% if eintrag.weg is defined and eintrag.weg %} <span class="muted">· {{ eintrag.weg }}</span>{% endif %}{% endif %}
|
||||
</p>
|
||||
{% if eintrag.zeit is defined %}<p class="mail-entry-time">{{ eintrag.zeit }} UTC</p>{% endif %}
|
||||
{% if not eintrag.ok %}
|
||||
{% if eintrag.hinweis %}<p class="mail-entry-hint">{{ eintrag.hinweis }}</p>{% endif %}
|
||||
<details class="mail-entry-detail"><summary>Meldung des Servers</summary><pre>{{ eintrag.meldung }}</pre></details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
@@ -10,59 +10,43 @@
|
||||
{% set f = mailForm ?? {} %}
|
||||
{% set e = mail.einstellung %}
|
||||
{% set weg = f.weg ?? e.weg %}
|
||||
<div class="card" id="mail">
|
||||
<div class="card-head"><h2>Versandweg</h2><span class="muted">gilt für alle Formulare</span></div>
|
||||
<div class="card-body">
|
||||
<dl class="facts">
|
||||
<dt>Eingestellt</dt>
|
||||
<dd>
|
||||
{% if mail.gueltig %}
|
||||
<strong>{{ mail.art }}</strong>{% if mail.ziel %} <code>{{ mail.ziel }}</code>{% endif %}{% if mail.benutzer %} <span class="muted">· angemeldet als</span> <code>{{ mail.benutzer }}</code>{% endif %}
|
||||
{% else %}
|
||||
<span class="check-bad"><strong>Die Einstellung ist ungültig.</strong></span>
|
||||
{% endif %}
|
||||
<br><span class="muted">Gespeichert in <code>{{ mail.quelle }}</code> als <code>MAILER_DSN</code>.{% if mail.quelle != '.env' %} Sie hat dort Vorrang; speichern lässt sich hier nichts, testen schon.{% endif %}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
{% set beschreibung = {
|
||||
smtp: 'Empfohlen. Ein echtes Postfach, unabhängig vom Hoster.',
|
||||
native: 'Über den Server. Der Absender muss zu einer Domain beim Hoster gehören.',
|
||||
'sendmail-t': 'Über den Server, falls mail() fehlt.',
|
||||
'sendmail-bs': 'Über den Server, bei manchen Hostern gesperrt.',
|
||||
} %}
|
||||
|
||||
{% if mailNotice %}
|
||||
<p class="notice {{ mailNotice.art == 'ok' ? 'ok' : 'error' }} mail-result" role="{{ mailNotice.art == 'ok' ? 'status' : 'alert' }}">{{ mailNotice.text }}</p>
|
||||
<p class="notice {{ mailNotice.art == 'ok' ? 'ok' : 'error' }}" role="{{ mailNotice.art == 'ok' ? 'status' : 'alert' }}">{{ mailNotice.text }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if mailResult %}
|
||||
<div class="notice {{ mailResult.ok ? 'ok' : 'error' }} mail-result" role="{{ mailResult.ok ? 'status' : 'alert' }}">
|
||||
<p><strong>{{ mailResult.ok ? 'Testmail an ' ~ mailResult.an ~ ' vom Mailserver angenommen' : 'Testmail an ' ~ mailResult.an ~ ' nicht versendet' }}</strong>{% if mailResult.weg is defined %} <span>({{ mailResult.weg }})</span>{% endif %}.{% if not mailResult.ok %} {{ mailResult.meldung }}{% endif %}</p>
|
||||
{% if mailResult.hinweis %}<p>{{ mailResult.hinweis }}</p>{% endif %}
|
||||
{% if mailResult.dialog %}<details><summary>Antworten des Mailservers</summary><pre>{{ mailResult.dialog }}</pre></details>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if mailResults %}
|
||||
<div class="mail-result">
|
||||
<p><strong>Alle Wege ausprobiert</strong> <span class="muted">– an {{ (f.an ?? '') }}. Was angenommen wurde, kann gespeichert werden; angekommen ist es damit noch nicht, bitte im Postfach nachsehen.</span></p>
|
||||
<ul class="log-list">
|
||||
{% for r in mailResults %}
|
||||
<li class="{{ r.ok ? 'check-ok' : 'check-bad' }}">
|
||||
<strong>{{ r.ok ? 'Angenommen' : 'Fehlgeschlagen' }}</strong> · {{ r.weg }}
|
||||
{% if not r.ok %}<br><code>{{ r.meldung }}</code>{% if r.hinweis %}<br><span class="muted">{{ r.hinweis }}</span>{% endif %}{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form class="mail-form" action="{{ path('admin_mail_save') }}" method="post">
|
||||
<form class="card mail-form" id="mail" action="{{ path('admin_mail_save') }}" method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||||
<fieldset>
|
||||
<legend>Versandweg</legend>
|
||||
<label class="check-label" for="weg-smtp"><input id="weg-smtp" type="radio" name="weg" value="smtp"{% if weg == 'smtp' %} checked{% endif %}> <span>SMTP mit Postfach <span class="muted">– empfohlen, unabhängig vom Hoster</span></span></label>
|
||||
<div class="card-head"><h2>Versandweg</h2><span class="muted">gilt für alle Formulare</span></div>
|
||||
<div class="card-body">
|
||||
<div class="mail-current">
|
||||
<span class="muted">Eingestellt</span>
|
||||
{% if mail.gueltig %}
|
||||
<strong>{{ mail.art }}</strong>{% if mail.ziel %} <code>{{ mail.ziel }}</code>{% endif %}{% if mail.benutzer %} <span class="muted">als</span> <code>{{ mail.benutzer }}</code>{% endif %}
|
||||
{% else %}
|
||||
<strong class="danger-text">Die Einstellung ist ungültig.</strong>
|
||||
{% endif %}
|
||||
<span class="muted mail-current-source">in <code>{{ mail.quelle }}</code> als <code>MAILER_DSN</code>{% if mail.quelle != '.env' %} – hat Vorrang, speichern lässt sich hier nichts, testen schon{% endif %}</span>
|
||||
</div>
|
||||
|
||||
<fieldset class="card-section">
|
||||
<legend>Weg wählen</legend>
|
||||
<div class="option-list">
|
||||
<label class="option" for="weg-smtp"><input id="weg-smtp" type="radio" name="weg" value="smtp"{% if weg == 'smtp' %} checked{% endif %}><span><strong>SMTP mit Postfach</strong><span>{{ beschreibung.smtp }}</span></span></label>
|
||||
{% for key, w in mail.wege %}
|
||||
<label class="check-label" for="weg-{{ key }}"><input id="weg-{{ key }}" type="radio" name="weg" value="{{ key }}"{% if weg == key %} checked{% endif %}> {{ w.label }}</label>
|
||||
<label class="option" for="weg-{{ key }}"><input id="weg-{{ key }}" type="radio" name="weg" value="{{ key }}"{% if weg == key %} checked{% endif %}><span><strong>{{ w.label }}</strong><span>{{ beschreibung[key] ?? '' }}</span></span></label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="smtp-fields">
|
||||
<legend>SMTP</legend>
|
||||
<fieldset class="card-section smtp-fields">
|
||||
<legend>SMTP-Zugang</legend>
|
||||
<div class="mail-grid">
|
||||
<div>
|
||||
<label for="mail-server">Server</label>
|
||||
@@ -84,7 +68,7 @@
|
||||
<p class="field-help" id="mail-port-help">587 mit STARTTLS ist üblich (IONOS, Microsoft 365: <code>smtp.office365.com</code>), 465 verschlüsselt von Anfang an. Das Passwort wird nie angezeigt und landet nur in <code>.env</code>.</p>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<fieldset class="card-section">
|
||||
<legend>Testmail</legend>
|
||||
<div class="mail-grid">
|
||||
<div>
|
||||
@@ -97,33 +81,64 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="field-help" id="mail-from-help">Derselbe Absender wie bei den Formularen. Über den Server (mail(), sendmail) muss er zu einer eigenen Domain beim Hoster gehören, bei SMTP ist es meist das angemeldete Postfach.</p>
|
||||
</fieldset>
|
||||
|
||||
<div class="mail-actions">
|
||||
<div class="button-row">
|
||||
<button class="secondary" type="submit" name="aktion" value="test">Diesen Weg testen</button>
|
||||
<button class="secondary" type="submit" name="aktion" value="alle">Alle Wege testen</button>
|
||||
{% if mail.quelle == '.env' %}<button type="submit" name="aktion" value="speichern">Diesen Weg speichern</button>{% endif %}
|
||||
</div>
|
||||
<p class="field-help">„Alle Wege testen“ schickt je Weg eine Testmail – bis zu vier. Gespeichert wird {{ mail.quelle == '.env' ? 'nur mit „Diesen Weg speichern“' : 'hier nichts, weil MAILER_DSN in der Server-Umgebung steht' }}.</p>
|
||||
<p class="field-help">„Alle Wege testen“ schickt je Weg eine Testmail – bis zu vier.</p>
|
||||
|
||||
{% if mailResult %}
|
||||
<div class="notice {{ mailResult.ok ? 'ok' : 'error' }} mail-result" role="{{ mailResult.ok ? 'status' : 'alert' }}">
|
||||
<p><strong>{{ mailResult.ok ? 'Testmail an ' ~ mailResult.an ~ ' vom Mailserver angenommen' : 'Testmail an ' ~ mailResult.an ~ ' nicht versendet' }}</strong>{% if mailResult.weg is defined %} ({{ mailResult.weg }}){% endif %}.{% if not mailResult.ok %} {{ mailResult.meldung }}{% endif %}</p>
|
||||
{% if mailResult.hinweis %}<p>{{ mailResult.hinweis }}</p>{% endif %}
|
||||
{% if mailResult.dialog %}<details><summary>Antworten des Mailservers</summary><pre>{{ mailResult.dialog }}</pre></details>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if mailResults %}
|
||||
<div class="mail-result">
|
||||
<p class="mail-result-head"><strong>Alle Wege ausprobiert</strong> <span class="muted">– an {{ f.an ?? '' }}. Angenommen heißt noch nicht angekommen: bitte im Postfach nachsehen.</span></p>
|
||||
<ul class="mail-log">
|
||||
{% for r in mailResults %}
|
||||
{% include 'admin/_mail_entry.html.twig' with {eintrag: r, titel: r.weg} only %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="card-foot form-foot">
|
||||
{% if mail.quelle == '.env' %}
|
||||
<button class="primary" type="submit" name="aktion" value="speichern">Diesen Weg speichern</button>
|
||||
<span class="muted">Erst mit „Speichern“ versenden die Formulare auf dem gewählten Weg.</span>
|
||||
{% else %}
|
||||
<span class="muted">MAILER_DSN steht in der Server-Umgebung – speichern lässt sich hier nichts.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h2>Letzte Versuche</h2><span class="muted">var/log/mail.log</span></div>
|
||||
<div class="card-head">
|
||||
<h2>Letzte Versuche</h2>
|
||||
{% if mail.protokoll is empty %}
|
||||
<span class="muted">var/log/mail.log</span>
|
||||
{% else %}
|
||||
<form action="{{ path('admin_mail_clear') }}" method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||||
<button class="text-button" type="submit">Protokoll leeren</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if mail.protokoll is empty %}
|
||||
<p class="muted">Noch nichts versendet – weder Testmail noch Formular.</p>
|
||||
{% else %}
|
||||
<ul class="log-list">
|
||||
<ul class="mail-log">
|
||||
{% for eintrag in mail.protokoll %}
|
||||
<li class="{{ eintrag.ok ? 'check-ok' : 'check-bad' }}">
|
||||
<strong>{{ eintrag.ok ? 'Angenommen' : 'Fehlgeschlagen' }}</strong> · {{ eintrag.zeit }} UTC · {{ eintrag.anlass }} an <code>{{ eintrag.an }}</code>{% if eintrag.von %} von <code>{{ eintrag.von }}</code>{% endif %}{% if eintrag.weg is defined and eintrag.weg %} · {{ eintrag.weg }}{% endif %}
|
||||
{% if not eintrag.ok %}<br><code>{{ eintrag.meldung }}</code>{% if eintrag.hinweis %}<br><span class="muted">{{ eintrag.hinweis }}</span>{% endif %}{% endif %}
|
||||
</li>
|
||||
{% include 'admin/_mail_entry.html.twig' with {eintrag: eintrag} only %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="field-help">Die letzten zehn Versuche aus <code>var/log/mail.log</code>, neueste zuerst. Zeiten in UTC.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,10 +47,13 @@
|
||||
{% 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>
|
||||
{# Unterpunkte von System: eigene Seiten, damit „System“ nicht alles trägt. #}
|
||||
{# Unterpunkte von System: eigene Seiten, damit „System“ nicht alles trägt. Sichtbar nur
|
||||
im Bereich System, sonst stünde das Menü dauerhaft aufgeklappt. #}
|
||||
{% if nav|default('') in ['system', 'mail'] %}
|
||||
<ul class="nav-sub">
|
||||
<li><a class="nav-item" href="{{ path('admin_mail') }}"{{ nav|default('') == 'mail' ? ' aria-current="page"' : '' }}>E-Mail-Versand</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</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_plugins') }}"{{ nav|default('') == 'plugins' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'plugins'} only %}Plugins</a></li>{% endif %}
|
||||
|
||||
@@ -268,22 +268,22 @@
|
||||
<div class="card-body">
|
||||
<p class="muted">Ein Inhaltspaket enthält Seiten mit allen Fassungen, Bereiche, Menüs, Medien und PDFs, Formulare, Weiterleitungen, Kategorien, Übersetzungen, Design und Einstellungen. Nicht darin: Anfragen, Statistik, Zugangsdaten zu Diensten (Turnstile) und – ohne Häkchen – die Konten.</p>
|
||||
|
||||
<form class="card-section" action="{{ path('admin_package_export') }}" method="post">
|
||||
<h3>Exportieren</h3>
|
||||
<form action="{{ path('admin_package_export') }}" method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||||
<label class="check-label"><input type="checkbox" name="benutzer" value="1"> Konten mit Passwörtern mitnehmen</label>
|
||||
<p class="field-help">Nur nötig, wenn die andere Website noch kein Konto hat. Das Paket enthält dann die Passwort-Hashes – nicht weitergeben.</p>
|
||||
<button class="secondary" type="submit">Paket herunterladen</button>
|
||||
<div class="button-row"><button class="secondary" type="submit">Paket herunterladen</button></div>
|
||||
</form>
|
||||
|
||||
<form class="card-section" action="{{ path('admin_package_import') }}" method="post" enctype="multipart/form-data">
|
||||
<h3>Importieren</h3>
|
||||
<form action="{{ path('admin_package_import') }}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||||
<label for="paket-datei">Paket hochladen (.zip)</label>
|
||||
<label class="field-first" for="paket-datei">Paket hochladen (.zip)</label>
|
||||
<input id="paket-datei" type="file" name="paket" accept=".zip,application/zip">
|
||||
<p class="field-help">Höchstens {{ uploadLimit }} je Upload (Einstellung des Hostings). Größere Pakete per FTP nach <code>var/pakete/</code> legen, dann erscheinen sie hier zur Auswahl.</p>
|
||||
{% if packages is not empty %}
|
||||
<fieldset class="field-group">
|
||||
<fieldset class="field-set">
|
||||
<legend>Oder ein Paket aus var/pakete/</legend>
|
||||
{% for package in packages %}
|
||||
<label class="check-label"><input type="radio" name="wartend" value="{{ package.name }}">
|
||||
@@ -296,7 +296,7 @@
|
||||
<p class="field-help">Ohne Häkchen bleiben Benutzer und Passwörter dieser Website, wie sie sind.</p>
|
||||
<label class="check-label"><input type="checkbox" name="bestaetigt" value="1" required> Ich weiß, dass die Inhalte dieser Website ersetzt werden.</label>
|
||||
<p class="field-help">Vorher wird automatisch eine Sicherung angelegt; während des Imports antwortet die Website mit der Wartungsseite.</p>
|
||||
<button class="primary" type="submit">Paket importieren</button>
|
||||
<div class="button-row"><button class="primary" type="submit">Paket importieren</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -412,6 +412,21 @@ final class SystemScreenTest extends WebTestCase
|
||||
$client->followRedirect();
|
||||
self::assertStringContainsString('in der Server-Umgebung gesetzt', (string) $client->getResponse()->getContent());
|
||||
|
||||
// Umut: „Logs leeren Funktion brauch ich“ - nur mit Token, danach ist die Liste leer.
|
||||
$client->request('POST', '/admin/system/e-mail/protokoll', ['_csrf_token' => 'erfunden']);
|
||||
self::assertFileExists($this->path.'-mail.log', 'Ohne Token bleibt das Protokoll.');
|
||||
$client->request('POST', '/admin/system/e-mail/protokoll', ['_csrf_token' => $token]);
|
||||
self::assertFileDoesNotExist($this->path.'-mail.log');
|
||||
$client->followRedirect();
|
||||
$html = (string) $client->getResponse()->getContent();
|
||||
self::assertStringContainsString('Das Protokoll ist leer.', $html);
|
||||
self::assertStringContainsString('Noch nichts versendet', $html);
|
||||
self::assertStringNotContainsString('Protokoll leeren', $html, 'Ohne Einträge gibt es nichts zu leeren.');
|
||||
|
||||
// Der Unterpunkt steht nur im Bereich System offen, nicht dauerhaft.
|
||||
$client->request('GET', '/admin/settings');
|
||||
self::assertStringNotContainsString('href="/admin/system/e-mail"', (string) $client->getResponse()->getContent());
|
||||
|
||||
// Nur für Administratoren, und nur mit Token.
|
||||
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => 'erfunden', 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'to' => 'umut@cms.test']);
|
||||
self::assertEmailCount(0);
|
||||
@@ -420,6 +435,8 @@ final class SystemScreenTest extends WebTestCase
|
||||
self::assertSame(403, $redaktion->getResponse()->getStatusCode());
|
||||
$redaktion->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'to' => 'umut@cms.test']);
|
||||
self::assertSame(403, $redaktion->getResponse()->getStatusCode());
|
||||
$redaktion->request('POST', '/admin/system/e-mail/protokoll', ['_csrf_token' => $token]);
|
||||
self::assertSame(403, $redaktion->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
private function login(string $username = 'admin'): KernelBrowser
|
||||
|
||||
Reference in new issue
Block a user