feat: formulaire de contact (CSRF + honeypot + rate-limit)
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
// Session pour CSRF et rate-limit
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$contactEmail = $_ENV['CONTACT_EMAIL'] ?? '';
|
||||
$error = null;
|
||||
$success = false;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF
|
||||
$token = $_POST['_token'] ?? '';
|
||||
if (!hash_equals($_SESSION['contact_csrf'] ?? '', $token)) {
|
||||
$error = 'Requête invalide. Veuillez réessayer.';
|
||||
}
|
||||
|
||||
// Honeypot (champ caché que les bots remplissent)
|
||||
if (!$error && ($_POST['_hp'] ?? '') !== '') {
|
||||
$error = 'Requête invalide.';
|
||||
}
|
||||
|
||||
// Rate-limit : 1 message par 5 minutes par session
|
||||
if (!$error) {
|
||||
$lastSent = $_SESSION['contact_last_sent'] ?? 0;
|
||||
if (time() - $lastSent < 300) {
|
||||
$error = 'Merci d\'attendre quelques minutes avant d\'envoyer un nouveau message.';
|
||||
}
|
||||
}
|
||||
|
||||
// Validation des champs
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$from = trim($_POST['email'] ?? '');
|
||||
$subject = trim($_POST['subject'] ?? 'Contact depuis varlog');
|
||||
$body = trim($_POST['message'] ?? '');
|
||||
|
||||
if (!$error) {
|
||||
if ($name === '' || mb_strlen($name) > 100) {
|
||||
$error = 'Nom invalide.';
|
||||
} elseif (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
|
||||
$error = 'Adresse e-mail invalide.';
|
||||
} elseif ($body === '' || mb_strlen($body) > 5000) {
|
||||
$error = 'Message vide ou trop long (max 5000 caractères).';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error && $contactEmail !== '') {
|
||||
$subjectClean = mb_encode_mimeheader(
|
||||
'[varlog contact] ' . mb_strimwidth($subject, 0, 100, '…'),
|
||||
'UTF-8',
|
||||
'B'
|
||||
);
|
||||
$nameClean = mb_encode_mimeheader($name, 'UTF-8', 'B');
|
||||
|
||||
$headers = 'From: =?UTF-8?B?' . base64_encode('varlog contact') . "?= <noreply@varlog>\r\n";
|
||||
$headers .= "Reply-To: {$nameClean} <{$from}>\r\n";
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
$headers .= "Content-Transfer-Encoding: 8bit\r\n";
|
||||
|
||||
$fullBody = "De : {$name} <{$from}>\n\n{$body}\n\n---\nEnvoyé depuis varlog";
|
||||
|
||||
if (@mail($contactEmail, $subjectClean, $fullBody, $headers)) {
|
||||
$_SESSION['contact_last_sent'] = time();
|
||||
$success = true;
|
||||
} else {
|
||||
$error = 'Erreur lors de l\'envoi. Veuillez réessayer plus tard.';
|
||||
}
|
||||
} elseif (!$error) {
|
||||
$error = 'Formulaire de contact non configuré.';
|
||||
}
|
||||
}
|
||||
|
||||
// Génère un nouveau token CSRF à chaque affichage du formulaire
|
||||
if (!$success) {
|
||||
$_SESSION['contact_csrf'] = bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="posts-list">
|
||||
<h1 class="mb-1">Contact</h1>
|
||||
<p class="text-muted mb-4">Envoyez-moi un message. Votre adresse e-mail ne sera pas publiée.</p>
|
||||
|
||||
<?php if ($success): ?>
|
||||
|
||||
<div class="alert alert-success" role="alert">
|
||||
Message envoyé. Je vous répondrai dès que possible.
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger" role="alert"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="route.php?action=contact" novalidate>
|
||||
<input type="hidden" name="_token" value="<?= htmlspecialchars($_SESSION['contact_csrf']) ?>">
|
||||
<!-- Honeypot -->
|
||||
<div style="display:none" aria-hidden="true">
|
||||
<input type="text" name="_hp" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contact-name" class="form-label">Nom <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="contact-name" name="name"
|
||||
value="<?= htmlspecialchars($_POST['name'] ?? '') ?>"
|
||||
maxlength="100" required autocomplete="name">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contact-email" class="form-label">Adresse e-mail <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" id="contact-email" name="email"
|
||||
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>"
|
||||
required autocomplete="email">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contact-subject" class="form-label">Sujet</label>
|
||||
<input type="text" class="form-control" id="contact-subject" name="subject"
|
||||
value="<?= htmlspecialchars($_POST['subject'] ?? '') ?>"
|
||||
maxlength="150" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="contact-message" class="form-label">Message <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="contact-message" name="message"
|
||||
rows="7" maxlength="5000" required><?= htmlspecialchars($_POST['message'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success">Envoyer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Contact — varlog';
|
||||
include __DIR__ . '/layout.php';
|
||||
Reference in New Issue
Block a user