diff --git a/docs/STATUS.md b/docs/STATUS.md index aeac239..8173220 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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. diff --git a/public/assets/admin.css b/public/assets/admin.css index 5411341..abe1ee6 100644 --- a/public/assets/admin.css +++ b/public/assets/admin.css @@ -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 } } diff --git a/src/Controller/MailController.php b/src/Controller/MailController.php index 2b2600f..9f218a9 100644 --- a/src/Controller/MailController.php +++ b/src/Controller/MailController.php @@ -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 { diff --git a/src/Ops/MailCheck.php b/src/Ops/MailCheck.php index c6549bd..8cfc239 100644 --- a/src/Ops/MailCheck.php +++ b/src/Ops/MailCheck.php @@ -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; diff --git a/templates/admin/_mail_entry.html.twig b/templates/admin/_mail_entry.html.twig new file mode 100644 index 0000000..6398526 --- /dev/null +++ b/templates/admin/_mail_entry.html.twig @@ -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. #} +
  • + {{ eintrag.ok ? 'Angenommen' : 'Fehlgeschlagen' }} +
    +

    + {% if titel is defined %}{{ titel }}{% else %}{{ eintrag.anlass }} an {{ eintrag.an }}{% if eintrag.von %} von {{ eintrag.von }}{% endif %}{% if eintrag.weg is defined and eintrag.weg %} · {{ eintrag.weg }}{% endif %}{% endif %} +

    + {% if eintrag.zeit is defined %}

    {{ eintrag.zeit }} UTC

    {% endif %} + {% if not eintrag.ok %} + {% if eintrag.hinweis %}

    {{ eintrag.hinweis }}

    {% endif %} +
    Meldung des Servers
    {{ eintrag.meldung }}
    + {% endif %} +
    +
  • diff --git a/templates/admin/mail.html.twig b/templates/admin/mail.html.twig index b7a192f..1c2150c 100644 --- a/templates/admin/mail.html.twig +++ b/templates/admin/mail.html.twig @@ -10,120 +10,135 @@ {% set f = mailForm ?? {} %} {% set e = mail.einstellung %} {% set weg = f.weg ?? e.weg %} -
    +{% 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 %} +

    {{ mailNotice.text }}

    +{% endif %} + +
    +

    Versandweg

    gilt für alle Formulare
    -
    -
    Eingestellt
    -
    - {% if mail.gueltig %} - {{ mail.art }}{% if mail.ziel %} {{ mail.ziel }}{% endif %}{% if mail.benutzer %} · angemeldet als {{ mail.benutzer }}{% endif %} - {% else %} - Die Einstellung ist ungültig. - {% endif %} -
    Gespeichert in {{ mail.quelle }} als MAILER_DSN.{% if mail.quelle != '.env' %} Sie hat dort Vorrang; speichern lässt sich hier nichts, testen schon.{% endif %} -
    -
    - - {% if mailNotice %} -

    {{ mailNotice.text }}

    - {% endif %} - - {% if mailResult %} -
    -

    {{ mailResult.ok ? 'Testmail an ' ~ mailResult.an ~ ' vom Mailserver angenommen' : 'Testmail an ' ~ mailResult.an ~ ' nicht versendet' }}{% if mailResult.weg is defined %} ({{ mailResult.weg }}){% endif %}.{% if not mailResult.ok %} {{ mailResult.meldung }}{% endif %}

    - {% if mailResult.hinweis %}

    {{ mailResult.hinweis }}

    {% endif %} - {% if mailResult.dialog %}
    Antworten des Mailservers
    {{ mailResult.dialog }}
    {% endif %} +
    + Eingestellt + {% if mail.gueltig %} + {{ mail.art }}{% if mail.ziel %} {{ mail.ziel }}{% endif %}{% if mail.benutzer %} als {{ mail.benutzer }}{% endif %} + {% else %} + Die Einstellung ist ungültig. + {% endif %} + in {{ mail.quelle }} als MAILER_DSN{% if mail.quelle != '.env' %} – hat Vorrang, speichern lässt sich hier nichts, testen schon{% endif %}
    - {% endif %} - {% if mailResults %} -
    -

    Alle Wege ausprobiert – an {{ (f.an ?? '') }}. Was angenommen wurde, kann gespeichert werden; angekommen ist es damit noch nicht, bitte im Postfach nachsehen.

    -
      - {% for r in mailResults %} -
    • - {{ r.ok ? 'Angenommen' : 'Fehlgeschlagen' }} · {{ r.weg }} - {% if not r.ok %}
      {{ r.meldung }}{% if r.hinweis %}
      {{ r.hinweis }}{% endif %}{% endif %} -
    • - {% endfor %} -
    -
    - {% endif %} - - - -
    - Versandweg - +
    + Weg wählen +
    + {% for key, w in mail.wege %} - + {% endfor %} -
    +
    + -
    - SMTP -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    +
    + SMTP-Zugang +
    +
    + +
    -

    587 mit STARTTLS ist üblich (IONOS, Microsoft 365: smtp.office365.com), 465 verschlüsselt von Anfang an. Das Passwort wird nie angezeigt und landet nur in .env.

    -
    - -
    - Testmail -
    -
    - - -
    -
    - - -
    +
    + +
    -

    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.

    -
    +
    + + +
    +
    + + +
    +
    +

    587 mit STARTTLS ist üblich (IONOS, Microsoft 365: smtp.office365.com), 465 verschlüsselt von Anfang an. Das Passwort wird nie angezeigt und landet nur in .env.

    +
    -
    +
    + Testmail +
    +
    + + +
    +
    + + +
    +
    +

    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.

    +
    - {% if mail.quelle == '.env' %}{% endif %}
    -

    „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' }}.

    - +

    „Alle Wege testen“ schickt je Weg eine Testmail – bis zu vier.

    + {% if mailResult %} +
    +

    {{ mailResult.ok ? 'Testmail an ' ~ mailResult.an ~ ' vom Mailserver angenommen' : 'Testmail an ' ~ mailResult.an ~ ' nicht versendet' }}{% if mailResult.weg is defined %} ({{ mailResult.weg }}){% endif %}.{% if not mailResult.ok %} {{ mailResult.meldung }}{% endif %}

    + {% if mailResult.hinweis %}

    {{ mailResult.hinweis }}

    {% endif %} + {% if mailResult.dialog %}
    Antworten des Mailservers
    {{ mailResult.dialog }}
    {% endif %} +
    + {% endif %} + + {% if mailResults %} +
    +

    Alle Wege ausprobiert – an {{ f.an ?? '' }}. Angenommen heißt noch nicht angekommen: bitte im Postfach nachsehen.

    +
      + {% for r in mailResults %} + {% include 'admin/_mail_entry.html.twig' with {eintrag: r, titel: r.weg} only %} + {% endfor %} +
    +
    + {% endif %} +
    -
    +
    + {% if mail.quelle == '.env' %} + + Erst mit „Speichern“ versenden die Formulare auf dem gewählten Weg. + {% else %} + MAILER_DSN steht in der Server-Umgebung – speichern lässt sich hier nichts. + {% endif %} +
    +
    -

    Letzte Versuche

    var/log/mail.log
    +
    +

    Letzte Versuche

    + {% if mail.protokoll is empty %} + var/log/mail.log + {% else %} +
    + + +
    + {% endif %} +
    {% if mail.protokoll is empty %}

    Noch nichts versendet – weder Testmail noch Formular.

    {% else %} -
      +
        {% for eintrag in mail.protokoll %} -
      • - {{ eintrag.ok ? 'Angenommen' : 'Fehlgeschlagen' }} · {{ eintrag.zeit }} UTC · {{ eintrag.anlass }} an {{ eintrag.an }}{% if eintrag.von %} von {{ eintrag.von }}{% endif %}{% if eintrag.weg is defined and eintrag.weg %} · {{ eintrag.weg }}{% endif %} - {% if not eintrag.ok %}
        {{ eintrag.meldung }}{% if eintrag.hinweis %}
        {{ eintrag.hinweis }}{% endif %}{% endif %} -
      • + {% include 'admin/_mail_entry.html.twig' with {eintrag: eintrag} only %} {% endfor %}
      +

      Die letzten zehn Versuche aus var/log/mail.log, neueste zuerst. Zeiten in UTC.

      {% endif %}
    diff --git a/templates/admin/shell.html.twig b/templates/admin/shell.html.twig index d3097b5..77c0192 100644 --- a/templates/admin/shell.html.twig +++ b/templates/admin/shell.html.twig @@ -47,10 +47,13 @@ {% if is_granted('ROLE_ADMIN') %}
  • {% include 'admin/icon.html.twig' with {name: 'theme'} only %}Design
  • {% endif %} {% if is_granted('ROLE_PUBLISHER') %}
  • {% include 'admin/icon.html.twig' with {name: 'settings'} only %}Einstellungen
  • {% endif %} {% if is_granted('ROLE_ADMIN') %}
  • {% include 'admin/icon.html.twig' with {name: 'system'} only %}System - {# 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'] %} + {% endif %}
  • {% endif %} {% if is_granted('ROLE_ADMIN') %}
  • {% include 'admin/icon.html.twig' with {name: 'tasks'} only %}Aufgaben
  • {% endif %} {% if is_granted('ROLE_ADMIN') %}
  • {% include 'admin/icon.html.twig' with {name: 'plugins'} only %}Plugins
  • {% endif %} diff --git a/templates/admin/system.html.twig b/templates/admin/system.html.twig index d843087..12be0ef 100644 --- a/templates/admin/system.html.twig +++ b/templates/admin/system.html.twig @@ -268,22 +268,22 @@

    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.

    -

    Exportieren

    -
    + +

    Exportieren

    Nur nötig, wenn die andere Website noch kein Konto hat. Das Paket enthält dann die Passwort-Hashes – nicht weitergeben.

    - +
    -

    Importieren

    -
    + +

    Importieren

    - +

    Höchstens {{ uploadLimit }} je Upload (Einstellung des Hostings). Größere Pakete per FTP nach var/pakete/ legen, dann erscheinen sie hier zur Auswahl.

    {% if packages is not empty %} -
    +
    Oder ein Paket aus var/pakete/ {% for package in packages %}
    diff --git a/tests/SystemScreenTest.php b/tests/SystemScreenTest.php index f9688b5..b551eab 100644 --- a/tests/SystemScreenTest.php +++ b/tests/SystemScreenTest.php @@ -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