Search by

tietge / silverstripe-widerrufsbutton

moritz-sauer-13

Elektronische Widerrufsfunktion nach § 356a BGB: Seitentyp, Formular, Eingangsbestätigung und CMS-Verwaltung der eingegangenen Widerrufe.

Package info

git.innomedia.de/Tietge/silverstripe-widerrufsbutton

Type:silverstripe-vendormodule

pkg:composer/tietge/silverstripe-widerrufsbutton

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

1.0.1 2026-10-08 06:59 UTC

This package is auto-updated.

Last update: 2026-10-08 06:59:15 UTC


README

Die elektronische Widerrufsfunktion nach § 356a BGB — seit dem 19.06.2026 Pflicht für jeden Shop, der Fernabsatzverträge über eine Online-Benutzeroberfläche schließt.

Das Modul bringt mit:

  • einen Seitentyp „Widerruf" mit redaktionellem Text, Formular und Danke-Seite,
  • ein DataObject je abgegebener Erklärung samt Eingangszeitpunkt,
  • die Eingangsbestätigung an den Verbraucher (Abs. 4: Inhalt der Erklärung, Datum, Uhrzeit) und eine Meldung an den Händler,
  • einen CMS-Bereich „Widerrufe" mit CSV-Export,
  • eine Zuordnung zum Vertrag über eine austauschbare Schnittstelle; für SilverShop ist eine Umsetzung dabei, die sich selbst einschaltet,
  • Schutz vor Missbrauch: Honigtopf, optional Captcha, Begrenzung der Absendungen je IP-Adresse, Doppelklick-Schutz sowie serverseitige Prüfung von Feldlängen und E-Mail-Adresse.

Voraussetzungen

  • Silverstripe 6 (silverstripe/framework, silverstripe/cms, silverstripe/admin, silverstripe/siteconfig), PHP ab 8.3.
  • Die globalen Klassen Page und PageController im Projekt (app/src/), wie sie jedes Projekt aus silverstripe/installer mitbringt. WiderrufPage und WiderrufPageController erben davon; ohne sie lädt das Modul nicht.
  • Spamschutz (optional): silverstripe/spamprotection plus ein Anbieter, etwa undefinedoffset/silverstripe-nocaptcha, und im Projekt der Anbieter als Voreinstellung:

    SilverStripe\SpamProtection\Extension\FormSpamProtectionExtension:
      default_spam_protector: UndefinedOffset\NoCaptcha\Forms\NocaptchaProtector
    

    Fehlt das Spamschutz-Modul, laufen Honigtopf und Begrenzung allein. Ist es installiert, aber kein default_spam_protector gesetzt, protokolliert das Modul einen Fehler und zeigt das Formular ohne Captcha – die Widerrufsseite darf nicht mit einem 500 ausfallen.

  • Mail-Layout (optional): Ist moritz-sauer-13/silverstripe-templated-emails (TemplatedMails\Model\TemplatedEmail) installiert, gehen beide Mails im Layout des Projekts raus. Sonst als schlichte HTML-Mail über SilverStripe\Control\Email\Email.
  • Ein funktionierender Mailversand. Umleitungen wie SS_SEND_ALL_EMAILS_TO greifen auch hier.

Installation

composer require tietge/silverstripe-widerrufsbutton
vendor/bin/sake db:build --flush

Danach im CMS eine Seite vom Typ Widerruf anlegen, im Tab „Widerruf“ den Ersatzweg (E-Mail-Adresse) und die Empfängeradresse für Meldungen eintragen und die Seite im Footer verlinken. Titel und Menütext sind mit „Vertrag widerrufen“ vorbelegt (Abs. 1).

Konfiguration

Alle Werte mit ihren Voreinstellungen – nur ändern, was abweichen soll:

Tietge\Widerruf\Pages\WiderrufPageController:
  # Honigtopf-Feld (verstecktes Feld, das nur ein Bot ausfüllt)
  use_honeypot: true
  # Ruft Form::enableSpamProtection(); welcher Anbieter greift, entscheidet das Projekt
  use_spam_protection: true
  # Höchstlänge des Freitextfelds „Betreffende Ware / Dienstleistung“ in Zeichen
  subject_max_length: 5000
  # Doppelklick-Schutz: dieselbe Erklärung innerhalb dieser Minuten nur einmal annehmen (0 = aus)
  duplicate_minutes: 10

Tietge\Widerruf\Service\CacheThrottle:
  # Begrenzung je IP-Adresse: höchstens `max` Absendungen in `decay_minutes` Minuten
  enabled: true
  max: 5
  decay_minutes: 10

Tietge\Widerruf\Model\Widerruf:
  # IP-Adresse und User-Agent mitschreiben. Für den Widerruf selbst nicht erforderlich.
  store_request_metadata: true

Mailadressen

  • Eingangsbestätigung (Abs. 4): geht an die im Formular eingegebene Adresse, sofort nach dem Speichern. Der Zeitpunkt des Versands steht am Datensatz (ConfirmationSentAt); im CMS fällt ein nicht versandter Nachweis sofort auf.
  • Meldung an den Händler: an die erste gefüllte Adresse aus dieser Reihe – das Feld „Meldung über neue Widerrufe an“ am Seitentyp, dann das Feld Email der SiteConfig (falls das Projekt eines hat), dann SilverStripe\Control\Email\Email.admin_email. Reply-To ist die Adresse des Verbrauchers.
  • Absender: die Voreinstellung des Frameworks (Email.admin_email bzw. die Mailer-Konfiguration des Projekts). Das Modul setzt keinen eigenen Absender.
  • Ersatzweg: die Adresse im Feld „Ersatzweg: E-Mail-Adresse“ am Seitentyp. Sie steht unter dem Formular und in jeder Ablehnung (Spamschutz, Begrenzung), damit niemand am Widerruf gehindert wird. Nicht leer lassen.

Begrenzung je IP-Adresse (Rate-Limit)

Jede angenommene Absendung löst zwei Mails aus. Damit das Formular keine Versandmaschine wird, nimmt das Modul von einer IP-Adresse höchstens max Absendungen in decay_minutes Minuten an (voreingestellt 5 in 10). Darüber hinaus bekommt der Absender eine Formularmeldung mit dem Ersatzweg; HTTP-Status bleibt 200.

  • Gezählt werden nur angenommene Absendungen – abgewiesene Versuche (Formularfehler, Honigtopf, Captcha) zählen nicht.
  • Das Zeitfenster ist fest: Die erste Absendung öffnet es, nach decay_minutes beginnt die nächste ein neues.
  • Die Zähler liegen im Silverstripe-Cache unter dem Dienst Psr\SimpleCache\CacheInterface.WiderrufThrottle; der Schlüssel ist ein Hash der Adresse, im Cache steht keine IP-Adresse im Klartext. Ein gestörter Cache lässt durch – lieber eine Mail zu viel als ein Widerruf zu wenig (Abs. 5).
  • Hinter einem Proxy oder Load Balancer liefert HTTPRequest::getIP() dessen Adresse, solange der Proxy nicht als vertrauenswürdig eingetragen ist (SS_TRUSTED_PROXY_IPS in der .env). Dann teilen sich alle Besucher eine Adresse und damit eine Begrenzung – vorher prüfen.

Eigene Zählung: Die Schnittstelle Tietge\Widerruf\Service\Throttle hat zwei Methoden, isLimited(string $ip): bool und hit(string $ip): void. Eine eigene Umsetzung wird über den Injector eingehängt:

SilverStripe\Core\Injector\Injector:
  Tietge\Widerruf\Service\Throttle:
    class: App\Widerruf\RedisThrottle

Ausnahmen ohne eigene Klasse: Der Controller ruft vor der Entscheidung den Erweiterungspunkt updateRateLimit(bool &$limited, string $ip, array $values). Eine Extension auf Tietge\Widerruf\Pages\WiderrufPageController kann $limited umdrehen – etwa für das eigene Büro oder angemeldete Kunden:

public function updateRateLimit(bool &$limited, string $ip, array $values): void
{
    if ($ip === '203.0.113.7') {
        $limited = false;
    }
}

Doppelklick-Schutz

Trifft dieselbe Erklärung – alle fünf Angaben gleich – innerhalb von duplicate_minutes ein zweites Mal ein, wird kein zweiter Datensatz angelegt und keine zweite Mail verschickt; der Absender landet auf der Bestätigungsseite der ersten Absendung. Schon eine andere Bestellnummer oder ein anderer Text gilt als eigene Erklärung. 0 schaltet den Schutz ab.

Feldlängen und E-Mail-Prüfung

Die Formularfelder tragen maxLength passend zur Datenbankspalte: Name und E-Mail-Adresse 255, Bestell- und Kundennummer 100 Zeichen (aus dem Datenmodell abgeleitet), der Freitext subject_max_length (5000). Der Browser begrenzt die Eingabe über maxlength; wer das umgeht, bekommt vom Server eine Formularmeldung statt eines Datenbankfehlers. Die E-Mail-Adresse wird nach derselben Regel geprüft, nach der später der Versand entscheidet (RFC-konform, streng).

Silverstripe 6 prüft Länge und E-Mail-Format zusätzlich über seine eigenen Feldvalidatoren, bringt dafür aber (Stand 6.2) keine deutschen Texte mit. Das Modul liefert sie in lang/de.yml nach – sie gelten damit projektweit – und verwendet dieselben Übersetzungsschlüssel, sodass jede Meldung nur einmal am Feld erscheint.

Honigtopf

Das Formular enthält ein Textfeld ContactByPost mit der Beschriftung „Postanschrift“, das samt Beschriftung in <div hidden aria-hidden="true"> steckt, nicht per Tabulator erreichbar ist und keine Ausfüllhilfe bekommt. Ein Mensch sieht es nie; ein Bot, der alle Felder füllt, verrät sich. Ist es gefüllt, wird die Absendung mit Hinweis auf den Ersatzweg abgewiesen und nichts gespeichert. Das Theme darf .widerruf-honeypot nicht sichtbar machen.

Bewusst keine Messung der Ausfüllzeit: Silverstripe baut das Formular beim Absenden neu auf, der Startzeitpunkt wäre immer „jetzt“ – eine Prüfung, die jeden Widerruf ablehnt.

Template anpassen

Das Modul rendert bewusst schmucklos. Gestaltet wird im Theme, indem das Template unter demselben Pfad überschrieben wird:

themes/<theme>/templates/Tietge/Widerruf/Pages/Layout/WiderrufPage.ss

Vorlage ist templates/Tietge/Widerruf/Pages/Layout/WiderrufPage.ss im Modul. Verfügbar sind $WiderrufForm (Formular mit der Klasse element-widerruf__form), $FallbackHint (Satz mit dem Ersatzweg), $CompletedWiderruf (der soeben abgegebene Widerruf – nur auf der Danke-Seite) und $ConfirmationContent (redaktioneller Text der Bestätigungsseite). Formular- und Feldmeldungen rendert das Standard-Formulartemplate von Silverstripe; das Theme stylt sie über .message.

Nicht antasten: die Beschriftung des Absende-Knopfes (kommt aus dem Controller, Abs. 3) und die Ausgabe des Eingangszeitpunkts auf der Bestätigungsseite (Teil der Eingangsbestätigung, Abs. 4).

Texte und Sprache

Alle Texte des Moduls – Feldbeschriftungen, Meldungen, Mails, CMS-Labels – sind fest deutsch und nicht über _t() übersetzbar; Mehrsprachigkeit ist offen. Einzige Ausnahme sind die Meldungen der Feldvalidatoren (Länge, E-Mail-Format), die über die Übersetzungsschlüssel des Frameworks laufen und deshalb bei einer anderen Locale englisch erscheinen.

Tests

Aus einem Projekt mit Silverstripe 6 heraus:

vendor/bin/phpunit vendor/tietge/silverstripe-widerrufsbutton/tests

WiderrufDuplicateTest legt eine temporäre Datenbank ss_tmpdb_* an (Zugangsdaten aus .env). Für den Lauf im Modul selbst liegt eine phpunit.xml.dist bei.

Wichtig

Das Absenden darf nie scheitern: nach § 356a Abs. 5 gilt die Erklärung als zugegangen, sobald sie versandt wurde. Der Datensatz wird deshalb geschrieben, bevor irgendetwas anderes passiert — eine unbekannte Bestellnummer ist eine Rückmeldung, kein Formularfehler. Nur Spamschutz und Begrenzung dürfen ablehnen, und beide nennen dabei den Ersatzweg.

Beide Beschriftungen sind gesetzlich vorgegeben („Vertrag widerrufen", „Widerruf bestätigen", jeweils mit Öffnungsklausel für Gleichbedeutendes). Der Knopf im Formular ist deshalb nicht über das CMS änderbar.