wip: Mobile-Theme-Testphase und PHP-8.2-Guards (Stand Juni 2026)

This commit is contained in:
Thomas Bartelt
2026-09-13 19:10:56 +02:00
parent 08beac8d67
commit 20d55f77b8
1448 changed files with 16015 additions and 583 deletions
+152 -52
View File
@@ -30,38 +30,104 @@ include_once './core/revocation.class.php';
$revocation = new Revocation($base_object);
$view = 'form';
$prefill = ['name' => '', 'order_number' => '', 'email' => ''];
$prefill = ['name' => '', 'email' => '', 'order_number' => '', 'article' => ''];
$error = null;
// ------------------------------------------------------------
// Tor-Exit-Liste — wird per Cron alle 4h aktualisiert.
// Datei in /var/cache/tor/exit-list.txt, eine IP pro Zeile.
// ------------------------------------------------------------
function widerruf_is_tor_exit($ip) {
static $set = null;
if ($set === null) {
$f = '/var/cache/tor/exit-list.txt';
if (file_exists($f) && (time() - filemtime($f)) < 172800) {
$arr = @file($f, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$set = $arr ? array_flip(array_map('trim', $arr)) : [];
} else {
$set = []; // fail-open wenn Liste fehlt/zu alt
}
}
return $ip !== '' && isset($set[$ip]);
}
$client_ip = $_SERVER['REMOTE_ADDR'] ?? '';
$is_tor = widerruf_is_tor_exit($client_ip);
// ------------------------------------------------------------
// Request dispatch
// ------------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$form_name = isset($_POST['name']) ? trim((string)$_POST['name']) : '';
$form_order_number = isset($_POST['order_number']) ? trim((string)$_POST['order_number']) : '';
$form_email = isset($_POST['email']) ? trim((string)$_POST['email']) : '';
$reason_id = isset($_POST['reason_id']) ? (int)$_POST['reason_id'] : 0;
$reason_text = isset($_POST['reason_text']) ? (string)$_POST['reason_text'] : '';
$confirm = isset($_POST['confirm']) && $_POST['confirm'] === '1';
$ip_now = $_SERVER['REMOTE_ADDR'] ?? '';
$ua_now = $_SERVER['HTTP_USER_AGENT'] ?? '';
// Eingaben für Fehlerfall ins Formular zurückspielen
$prefill = [
'name' => $form_name,
'order_number' => $form_order_number,
'email' => $form_email,
];
// ------------------------------------------------------------
// Bot-Schutz: tor-exit + honeypot + time-trap + session-token + rate-limit.
// Bei Bot-Verdacht still "success" zurückgeben (Bot soll nichts merken).
// ------------------------------------------------------------
$is_bot = false;
$bot_reason = '';
if (!$confirm) {
$view = 'form';
$error = 'no_confirm';
if ($is_tor) {
$is_bot = true; $bot_reason = 'tor-exit';
}
if (!$is_bot && (!empty($_POST['website']) || !empty($_POST['fax']))) {
$is_bot = true; $bot_reason = 'honeypot';
}
if (!$is_bot) {
$ts = isset($_POST['ts']) ? (int)$_POST['ts'] : 0;
$delta = time() - $ts;
if ($ts <= 0 || $delta < 4 || $delta > 7200) {
$is_bot = true; $bot_reason = 'timetrap delta='.$delta;
}
}
if (!$is_bot) {
if (empty($_POST['form_token']) || empty($_SESSION['widerruf_token'])
|| !hash_equals((string)$_SESSION['widerruf_token'], (string)$_POST['form_token'])) {
$is_bot = true; $bot_reason = 'token-mismatch';
}
}
if (!$is_bot) {
$rl_hash = sha1($ip_now.'|widerruf');
$rl_file = sys_get_temp_dir().'/widerruf_rl_'.$rl_hash;
$last = file_exists($rl_file) ? (int)@file_get_contents($rl_file) : 0;
if (time() - $last < 60) {
$is_bot = true; $bot_reason = 'ratelimit last='.$last;
} else {
@file_put_contents($rl_file, (string)time());
}
}
if ($is_bot) {
error_log('[widerruf] BOT BLOCKED reason='.$bot_reason.' ip='.$ip_now.' ua='.substr($ua_now, 0, 120));
// Token einmalig verbrauchen, damit derselbe nicht reusable ist
unset($_SESSION['widerruf_token']);
$view = 'success';
} else {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
// Token verbrauchen
unset($_SESSION['widerruf_token']);
$form_name = isset($_POST['name']) ? trim((string)$_POST['name']) : '';
$form_email = isset($_POST['email']) ? trim((string)$_POST['email']) : '';
$form_order_number = isset($_POST['order_number']) ? trim((string)$_POST['order_number']) : '';
$form_article = isset($_POST['article']) ? trim((string)$_POST['article']) : '';
$prefill = [
'name' => $form_name,
'email' => $form_email,
'order_number' => $form_order_number,
'article' => $form_article,
];
if ($revocation->submit_anonymous(
$form_name, $form_order_number, $form_email,
$reason_id, $reason_text, $ip, $ua
$form_article, $ip_now, $ua_now
)) {
$view = 'success';
} else {
@@ -72,33 +138,40 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else {
// GET — bei Token-Link: Felder vorausfüllen
$token = isset($_GET['t']) ? (string)$_GET['t'] : '';
if ($token !== '') {
$pf = $revocation->prefill_from_token($token);
if ($pf) {
$prefill = $pf;
} elseif ($revocation->get_error() === 'feature_disabled') {
$view = 'error';
$error = 'feature_disabled';
// GET — Tor-Exit-Nutzer:innen sehen statt Form einen Hinweis auf alternative Wege
if ($is_tor) {
error_log('[widerruf] TOR EXIT GET ip='.$client_ip);
$view = 'tor_blocked';
} else {
// Bei Token-Link: Felder vorausfüllen
$token = isset($_GET['t']) ? (string)$_GET['t'] : '';
if ($token !== '') {
$pf = $revocation->prefill_from_token($token);
if ($pf) {
$prefill = $pf;
} elseif ($revocation->get_error() === 'feature_disabled') {
$view = 'error';
$error = 'feature_disabled';
}
// Bei anderen Token-Fehlern: leeres Formular anbieten ohne Hinweistext
}
// Bei anderen Token-Fehlern: leeres Formular anbieten ohne Hinweistext
}
}
// Neuen Form-Token + Timestamp für Render erzeugen
if ($view === 'form') {
$_SESSION['widerruf_token'] = bin2hex(random_bytes(16));
$form_token_render = $_SESSION['widerruf_token'];
$form_ts_render = time();
}
$reasons = $revocation->fetch_reasons();
$messages = [
'no_confirm' => 'Bitte bestätigen Sie den Widerruf durch Setzen des Hakens.',
'missing_order' => 'Bitte geben Sie Ihre Bestellnummer an.',
'missing_name' => 'Bitte geben Sie Ihren Namen an.',
'invalid_email' => 'Bitte geben Sie eine gültige E-Mail-Adresse an.',
'order_not_found' => 'Wir konnten keine Bestellung mit dieser Bestellnummer und E-Mail-Adresse finden. '
. 'Bitte prüfen Sie Ihre Eingaben.',
'expired' => 'Die 14-tägige Widerrufsfrist ist überschritten. '
. 'Wenden Sie sich bitte direkt an uns.',
'already_submitted' => 'Für diese Bestellung wurde bereits ein Widerruf erklärt.',
'feature_disabled' => 'Die elektronische Widerrufsfunktion ist aktuell noch nicht aktiv.',
'not_eligible' => 'Für diese Bestellung steht der elektronische Widerruf nicht zur Verfügung.',
];
$display_error = isset($messages[$error]) ? $messages[$error] : null;
@@ -140,39 +213,66 @@ $display_error = isset($messages[$error]) ? $messages[$error] : null;
<h1>Vertrag widerrufen</h1>
<p>Gemäß § 355 BGB haben Sie das Recht, Ihren Vertrag binnen 14 Tagen
ohne Angabe von Gründen zu widerrufen. Füllen Sie bitte die folgenden
drei Pflichtfelder aus.</p>
ohne Angabe von Gründen zu widerrufen. Pflichtfelder sind <strong>Name</strong>
und <strong>E-Mail-Adresse</strong>. Bestellnummer und Artikel sind
freiwillige Angaben, die uns die Zuordnung erleichtern.</p>
<?php if ($display_error): ?><div class="err"><?= htmlspecialchars($display_error) ?></div><?php endif; ?>
<form method="post" action="<?= htmlspecialchars(strtok($_SERVER['REQUEST_URI'], '?')) ?>">
<form method="post" action="<?= htmlspecialchars(strtok($_SERVER['REQUEST_URI'], '?')) ?>" autocomplete="on">
<input type="hidden" name="ts" value="<?= (int)$form_ts_render ?>">
<input type="hidden" name="form_token" value="<?= htmlspecialchars($form_token_render) ?>">
<div aria-hidden="true" style="position:absolute;left:-9999px;top:-9999px;height:0;width:0;overflow:hidden;" tabindex="-1">
<label for="website_hp">Website (bitte leer lassen)</label>
<input type="text" id="website_hp" name="website" autocomplete="off" tabindex="-1" value="">
<label for="fax_hp">Fax</label>
<input type="text" id="fax_hp" name="fax" autocomplete="off" tabindex="-1" value="">
</div>
<label for="name">Vor- und Nachname</label>
<input type="text" id="name" name="name" required maxlength="120"
value="<?= htmlspecialchars($prefill['name']) ?>">
<label for="order_number">Bestellnummer</label>
<input type="text" id="order_number" name="order_number" required maxlength="32"
value="<?= htmlspecialchars($prefill['order_number']) ?>"
placeholder="z.B. 102345">
<label for="email">E-Mail-Adresse</label>
<input type="email" id="email" name="email" required maxlength="120"
value="<?= htmlspecialchars($prefill['email']) ?>"
placeholder="Die E-Mail, mit der Sie bestellt haben">
<label class="check">
<input type="checkbox" name="confirm" value="1" required>
Ich erkläre hiermit den Widerruf meiner oben genannten Bestellung.
</label>
<label for="order_number">Bestellnummer <span class="muted" style="font-weight:normal;">(optional &ndash; hilft uns bei der Zuordnung)</span></label>
<input type="text" id="order_number" name="order_number" maxlength="64"
value="<?= htmlspecialchars($prefill['order_number']) ?>"
placeholder="z.B. 102345, AB-2026-001 &ndash; falls zur Hand">
<button type="submit" class="btn">Vertrag widerrufen</button>
<label for="article">Artikel / Bemerkung <span class="muted" style="font-weight:normal;">(optional &ndash; Artikelbezeichnung, Bestelldatum o.&auml;.)</span></label>
<input type="text" id="article" name="article" maxlength="200"
value="<?= htmlspecialchars($prefill['article']) ?>"
placeholder="z.B. Trafo, Schalter, Bestellung vom 14.06.2026">
<p class="muted" style="margin-top:20px;">Nach dem Absenden erhalten Sie eine
Bestätigungs-E-Mail mit der Rücksendeadresse und den weiteren Informationen
zur Rücksendung.</p>
<button type="submit" class="btn" style="margin-top:24px;">Vertrag widerrufen</button>
<p class="muted" style="margin-top:20px;">Mit Klick auf den Button erkl&auml;ren Sie
den Widerruf Ihres Vertrags. Sie erhalten anschlie&szlig;end eine Eingangs&shy;best&auml;tigung
per E-Mail.</p>
</form>
<?php elseif ($view === 'tor_blocked'): ?>
<h1>Widerruf &uuml;ber anonyme Verbindung</h1>
<p>Sie scheinen &uuml;ber eine anonyme Verbindung (z.&nbsp;B. Tor) auf diese Seite zuzugreifen.
Aus Spam-Schutz-Gr&uuml;nden ist das Online-Formular &uuml;ber anonyme Netzwerke deaktiviert.</p>
<p>Ihr Widerrufsrecht bleibt selbstverst&auml;ndlich unver&auml;ndert.
Sie k&ouml;nnen Ihren Widerruf jederzeit auf einem der folgenden Wege erkl&auml;ren:</p>
<ul style="line-height:1.7;">
<li>per E-Mail an <a href="mailto:<?= htmlspecialchars(REVOCATION_OWNER_EMAIL) ?>"><?= htmlspecialchars(REVOCATION_OWNER_EMAIL) ?></a></li>
<li>per Brief an die im Impressum genannte Anschrift</li>
<li>mit unserem <a href="/Widerrufsformular-Muster/">Muster-Widerrufsformular</a> (Download / Ausdruck)</li>
</ul>
<p class="muted"><?= htmlspecialchars(REVOCATION_RETURN_COMPANY) ?> &middot;
<?= htmlspecialchars(REVOCATION_RETURN_STREET) ?> &middot;
<?= htmlspecialchars(REVOCATION_RETURN_CITY) ?></p>
<?php elseif ($view === 'success'): ?>
<h1>Widerruf entgegengenommen</h1>