feat: formulaire de contact (CSRF + honeypot + rate-limit)
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -140,6 +140,10 @@ switch ($action) {
|
|||||||
include BASE_PATH . '/templates/legal.php';
|
include BASE_PATH . '/templates/legal.php';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'contact':
|
||||||
|
include BASE_PATH . '/templates/contact.php';
|
||||||
|
break;
|
||||||
|
|
||||||
case 'licenses':
|
case 'licenses':
|
||||||
include BASE_PATH . '/templates/licenses.php';
|
include BASE_PATH . '/templates/licenses.php';
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="footer-nav" aria-label="Liens du site">
|
<nav class="footer-nav" aria-label="Liens du site">
|
||||||
<a href="route.php?action=about">À propos</a>
|
<a href="route.php?action=about">À propos</a>
|
||||||
|
<a href="route.php?action=contact">Contact</a>
|
||||||
<a href="route.php?action=legal">Mentions légales</a>
|
<a href="route.php?action=legal">Mentions légales</a>
|
||||||
<a href="route.php?action=licenses">Licences</a>
|
<a href="route.php?action=licenses">Licences</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@ ob_start();
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="mb-1"><strong>Responsable de publication :</strong> Cédric Abonnel</p>
|
<p class="mb-1"><strong>Responsable de publication :</strong> Cédric Abonnel</p>
|
||||||
<p class="mb-1"><strong>Qualité :</strong> Particulier — site personnel non commercial</p>
|
<p class="mb-1"><strong>Qualité :</strong> Particulier — site personnel non commercial</p>
|
||||||
<p class="mb-0"><strong>Contact :</strong> <a href="mailto:cedric.abonnel@gmail.com">cedric.abonnel@gmail.com</a></p>
|
<p class="mb-0"><strong>Contact :</strong> <a href="route.php?action=contact">formulaire de contact</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -99,7 +99,7 @@ ob_start();
|
|||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
Conformément au RGPD (règlement UE 2016/679), vous disposez d'un droit d'accès, de rectification
|
Conformément au RGPD (règlement UE 2016/679), vous disposez d'un droit d'accès, de rectification
|
||||||
et de suppression des données vous concernant. Pour exercer ces droits :
|
et de suppression des données vous concernant. Pour exercer ces droits :
|
||||||
<a href="mailto:cedric.abonnel@gmail.com">cedric.abonnel@gmail.com</a>.
|
<a href="route.php?action=contact">formulaire de contact</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user