tietge / silverstripe-widerrufsbutton
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
Requires
- php: ^8.3
- silverstripe/admin: ^3
- silverstripe/cms: ^6
- silverstripe/framework: ^6
- silverstripe/siteconfig: ^6
Requires (Dev)
- phpunit/phpunit: ^11
Suggests
- silvershop/core: Ordnet einen Widerruf automatisch der passenden Bestellung zu (SilverShopOrderResolver schaltet sich dann selbst ein).
- silverstripe/spamprotection: Schaltet die dritte Spamschutz-Lage frei (enableSpamProtection). Ohne das Modul laufen Honigtopf und Begrenzung je IP-Adresse allein.
Provides
None
Conflicts
None
Replaces
None
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
PageundPageControllerim Projekt (app/src/), wie sie jedes Projekt aussilverstripe/installermitbringt.WiderrufPageundWiderrufPageControllererben davon; ohne sie lädt das Modul nicht. Spamschutz (optional):
silverstripe/spamprotectionplus ein Anbieter, etwaundefinedoffset/silverstripe-nocaptcha, und im Projekt der Anbieter als Voreinstellung:SilverStripe\SpamProtection\Extension\FormSpamProtectionExtension: default_spam_protector: UndefinedOffset\NoCaptcha\Forms\NocaptchaProtectorFehlt das Spamschutz-Modul, laufen Honigtopf und Begrenzung allein. Ist es installiert, aber kein
default_spam_protectorgesetzt, 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 überSilverStripe\Control\Email\Email. - Ein funktionierender Mailversand. Umleitungen wie
SS_SEND_ALL_EMAILS_TOgreifen 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
Emailder SiteConfig (falls das Projekt eines hat), dannSilverStripe\Control\Email\Email.admin_email. Reply-To ist die Adresse des Verbrauchers. - Absender: die Voreinstellung des Frameworks (
Email.admin_emailbzw. 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_minutesbeginnt 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_IPSin 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.