Files
2026-09-13 11:58:12 +01:00

100 lines
3.1 KiB
PHP

<?php
session_start();
// Honeypot: if a bot filled in a hidden field, silently discard
if (!empty($_POST['website'])) {
header('Location: /?sent=1#contact');
exit;
}
// Rate Limiting: Session and IP-based cooldown
$ip = $_SERVER['REMOTE_ADDR'];
$cooldown_time = 300; // 5 minutes
// Session-based cooldown
if (isset($_SESSION['last_submission']) && (time() - $_SESSION['last_submission'] < $cooldown_time)) {
header('Location: /?error=too_fast#contact');
exit;
}
// IP-based cooldown using a temporary file
$ip_file = sys_get_temp_dir() . '/fraxle_rate_' . md5($ip);
if (file_exists($ip_file)) {
$last_hit = (int)file_get_contents($ip_file);
if ((time() - $last_hit) < $cooldown_time) {
header('Location: /?error=too_fast#contact');
exit;
}
}
file_put_contents($ip_file, time());
$name = str_replace(["\r", "\n"], '', trim($_POST['name'] ?? ''));
$email = str_replace(["\r", "\n"], '', trim($_POST['email'] ?? ''));
$message = str_replace(["\r", "\n"], '', trim($_POST['message'] ?? ''));
$captcha = intval($_POST['captcha'] ?? -9999);
$expected = intval($_SESSION['captcha_answer'] ?? -1);
// Always clear session captcha after use
unset($_SESSION['captcha_answer']);
// Validate
$csrf_valid = isset($_POST['csrf_token'], $_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $_POST['csrf_token']);
$fails = [];
if (!$csrf_valid) {
$fails[] = 'csrf-invalid';
}
if (empty($name) || strlen($name) > 200) {
$fails[] = 'name';
}
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$fails[] = 'email';
}
if (empty($message) || strlen($message) > 5000) {
$fails[] = 'message';
}
if ($expected === -1) {
$fails[] = 'captcha-no-session';
} elseif ($captcha !== $expected) {
$fails[] = 'captcha-mismatch';
}
if ($fails) {
error_log('fraxle contact form rejected: ' . implode(',', $fails)
. ' | got=' . $captcha . ' expected=' . $expected
. ' | sid=' . session_id()
. ' | host=' . ($_SERVER['HTTP_HOST'] ?? '?')
. ' | ref=' . ($_SERVER['HTTP_REFERER'] ?? '?'));
header('Location: /?error=details#contact');
exit;
}
// Sanitize for email body
$safeName = strip_tags($name);
$safeEmail = strip_tags($email);
$safeMessage = strip_tags($message);
$to = 'wizard@fraxle.net';
$subject = "Fraxle.net enquiry from $safeName";
$body = "You have a new enquiry from your website.\n\n"
. "Name: $safeName\n"
. "Email: $safeEmail\n"
. "Message:\n$safeMessage\n\n"
. "---\nSent via fraxle.net contact form";
$headers = "From: noreply@fraxle.net\r\n";
$headers .= "Reply-To: $safeEmail\r\n";
$headers .= "X-Mailer: PHP/" . phpversion() . "\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
if (mail($to, $subject, $body, $headers)) {
$_SESSION['last_submission'] = time();
header('Location: /?sent=1#contact');
} else {
error_log('fraxle contact form: mail() failed for ' . $safeEmail
. ' — ' . (error_get_last()['message'] ?? 'no PHP error reported'));
header('Location: /?error=mail#contact');
}
exit;