Files

304 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 &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">
<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">
<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>
<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>