304 lines
12 KiB
PHP
304 lines
12 KiB
PHP
<?php
|
||
/**
|
||
* Carteasy – Landingpage Widerruf (§ 356a BGB, ab 19.06.2026)
|
||
*
|
||
* GET → Leeres Formular
|
||
* GET ?t=<token> → Formular mit vorausgefüllten Feldern (Convenience aus Bestell-Mail)
|
||
* POST → Widerruf erfassen, Bestätigungsseite anzeigen
|
||
*
|
||
* Submit läuft IMMER über Bestellnr + E-Mail Lookup, nicht über Token.
|
||
*
|
||
* @copyright Wlanium / Thomas Bartelt
|
||
* @since 2026-04-19
|
||
*/
|
||
|
||
session_start();
|
||
ini_set('display_errors', 0);
|
||
error_reporting(0);
|
||
ini_set('log_errors', 1);
|
||
ini_set('error_log', __DIR__.'/error_log.txt');
|
||
|
||
header('X-Robots-Tag: noindex, nofollow', true);
|
||
header('Content-Security-Policy: default-src \'self\'; style-src \'unsafe-inline\'');
|
||
|
||
include_once './core/base.class.php';
|
||
include_once './core/registry.class.php';
|
||
$base_object = new base();
|
||
Registry::set('base', $base_object);
|
||
|
||
include_once './core/revocation.class.php';
|
||
$revocation = new Revocation($base_object);
|
||
|
||
$view = 'form';
|
||
$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') {
|
||
|
||
$ip_now = $_SERVER['REMOTE_ADDR'] ?? '';
|
||
$ua_now = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||
|
||
// ------------------------------------------------------------
|
||
// 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 ($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 {
|
||
// 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,
|
||
$form_article, $ip_now, $ua_now
|
||
)) {
|
||
$view = 'success';
|
||
} else {
|
||
$view = 'form';
|
||
$error = $revocation->get_error();
|
||
}
|
||
}
|
||
|
||
} else {
|
||
|
||
// 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
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 = [
|
||
'missing_name' => 'Bitte geben Sie Ihren Namen an.',
|
||
'invalid_email' => 'Bitte geben Sie eine gültige E-Mail-Adresse an.',
|
||
'already_submitted' => 'Für diese Bestellung wurde bereits ein Widerruf erklärt.',
|
||
'feature_disabled' => 'Die elektronische Widerrufsfunktion ist aktuell noch nicht aktiv.',
|
||
];
|
||
$display_error = isset($messages[$error]) ? $messages[$error] : null;
|
||
|
||
?><!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="robots" content="noindex,nofollow">
|
||
<title>Widerruf – Intelectra</title>
|
||
<style>
|
||
body {font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;
|
||
background:#f6f6f6;margin:0;color:#222;}
|
||
.wrap{max-width:640px;margin:40px auto;padding:32px;background:#fff;
|
||
border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.08);}
|
||
h1{margin-top:0;color:#c00;}
|
||
h2{margin-top:1.6em;}
|
||
.meta{background:#f4f4f4;padding:12px 16px;border-left:4px solid #c00;
|
||
font-size:.9em;margin:16px 0;}
|
||
label{display:block;margin:12px 0 4px;font-weight:600;}
|
||
select,textarea,input[type=text],input[type=email]{width:100%;padding:10px;font-size:1em;
|
||
border:1px solid #ccc;border-radius:4px;box-sizing:border-box;}
|
||
textarea{min-height:90px;font-family:inherit;}
|
||
.check{margin:20px 0;font-size:.95em;font-weight:normal;}
|
||
.btn{background:#c00;color:#fff;border:0;padding:14px 28px;
|
||
font-size:1.05em;font-weight:bold;border-radius:4px;cursor:pointer;}
|
||
.btn:hover{background:#a00;}
|
||
.err{background:#ffe9e9;border-left:4px solid #c00;padding:10px 14px;
|
||
margin:12px 0;}
|
||
.ok{background:#e7f7e7;border-left:4px solid #2a7;padding:14px 18px;}
|
||
.muted{color:#666;font-size:.85em;}
|
||
footer{text-align:center;margin:20px 0;color:#999;font-size:.8em;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
|
||
<?php if ($view === 'form'): ?>
|
||
|
||
<h1>Vertrag widerrufen</h1>
|
||
<p>Gemäß § 355 BGB haben Sie das Recht, Ihren Vertrag binnen 14 Tagen
|
||
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'], '?')) ?>" 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="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 for="order_number">Bestellnummer <span class="muted" style="font-weight:normal;">(optional – 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 – falls zur Hand">
|
||
|
||
<label for="article">Artikel / Bemerkung <span class="muted" style="font-weight:normal;">(optional – Artikelbezeichnung, Bestelldatum o.ä.)</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">
|
||
|
||
<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ären Sie
|
||
den Widerruf Ihres Vertrags. Sie erhalten anschließend eine Eingangs­bestätigung
|
||
per E-Mail.</p>
|
||
</form>
|
||
|
||
<?php elseif ($view === 'tor_blocked'): ?>
|
||
|
||
<h1>Widerruf über anonyme Verbindung</h1>
|
||
<p>Sie scheinen über eine anonyme Verbindung (z. B. Tor) auf diese Seite zuzugreifen.
|
||
Aus Spam-Schutz-Gründen ist das Online-Formular über anonyme Netzwerke deaktiviert.</p>
|
||
<p>Ihr Widerrufsrecht bleibt selbstverständlich unverändert.
|
||
Sie können Ihren Widerruf jederzeit auf einem der folgenden Wege erklä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) ?> ·
|
||
<?= htmlspecialchars(REVOCATION_RETURN_STREET) ?> ·
|
||
<?= htmlspecialchars(REVOCATION_RETURN_CITY) ?></p>
|
||
|
||
<?php elseif ($view === 'success'): ?>
|
||
|
||
<h1>Widerruf entgegengenommen</h1>
|
||
<div class="ok">
|
||
<strong>Vielen Dank.</strong> Ihre Widerrufserklärung wurde erfasst.
|
||
Sie erhalten in Kürze eine Bestätigungs-E-Mail an die hinterlegte
|
||
Adresse mit der Rücksendeadresse und den nächsten Schritten.
|
||
</div>
|
||
<p class="muted">Bei Rückfragen erreichen Sie uns unter
|
||
<a href="mailto:<?= htmlspecialchars(REVOCATION_OWNER_EMAIL) ?>"><?= htmlspecialchars(REVOCATION_OWNER_EMAIL) ?></a>.</p>
|
||
|
||
<?php else: ?>
|
||
|
||
<h1>Widerruf nicht möglich</h1>
|
||
<div class="err"><?= htmlspecialchars($display_error ?: 'Widerruf zurzeit nicht verarbeitbar.') ?></div>
|
||
<p>Sie können Ihren Widerruf jederzeit auch formlos per E-Mail an
|
||
<a href="mailto:<?= htmlspecialchars(REVOCATION_OWNER_EMAIL) ?>"><?= htmlspecialchars(REVOCATION_OWNER_EMAIL) ?></a>
|
||
oder per Brief erklären.</p>
|
||
<p class="muted"><?= htmlspecialchars(REVOCATION_RETURN_COMPANY) ?> ·
|
||
<?= htmlspecialchars(REVOCATION_RETURN_STREET) ?> ·
|
||
<?= htmlspecialchars(REVOCATION_RETURN_CITY) ?></p>
|
||
|
||
<?php endif; ?>
|
||
|
||
</div>
|
||
<footer>Intelectra Online-Shop · <a href="/">Zurück zum Shop</a></footer>
|
||
</body>
|
||
</html>
|