Feat: sécurisation formulaire white paper + stockage leads

- Honeypot, timing check, rate limiting IP, validation serveur
- Déduplication par email : enrichissement des champs vides si contact existant
- Blueprint white-paper : onglet "Contacts intéressés" (champ structure contactDatabase)
- Blueprint site.yml : ajout onglet "Données d'usage" pour vue globale des leads
- Route externalisée dans site/config/routes/download-white-paper.php
- isDownloadable côté client (prénom, nom, email valide, consentement)
- Cursor : pas de hover sur boutons disabled
- Buttons : hover désactivé si disabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-03-19 07:11:00 +01:00
parent 974067d986
commit 67d8159787
8 changed files with 312 additions and 112 deletions

View file

@ -13,25 +13,6 @@ return [
'thumbs' => require __DIR__ . '/thumbs.php',
'routes' => [
[
'pattern' => ['(:any)/(:any)/download', 'en/(:any)/(:any)/download'],
'method' => 'POST',
'action' => function (string $parent, string $slug) {
$page = kirby()->page($parent . '/' . $slug);
if (!$page || $page->intendedTemplate()->name() !== 'white-paper') {
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['error' => 'Not found']);
exit;
}
// TODO: store/email form data ($kirby->request()->body())
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'fileUrl' => $page->downloadFile()->toFile()?->url(),
]);
exit;
}
]
require(__DIR__ . '/routes/download-white-paper.php')
],
];

View file

@ -0,0 +1,99 @@
<?php
function wpReject(int $code, string $message): void {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode(['error' => $message]);
exit;
}
return [
'pattern' => ['(:any)/(:any)/download', 'en/(:any)/(:any)/download'],
'method' => 'POST',
'action' => function (string $parent, string $slug) {
$page = page($parent . '/' . $slug);
if (!$page || $page->intendedTemplate()->name() !== 'white-paper') {
wpReject(404, 'Not found');
}
$body = kirby()->request()->body()->toArray();
// ── Honeypot ──────────────────────────────────────────────
if (!empty($body['_hp'])) {
wpReject(400, 'Bad request');
}
// ── Timing check (min 3 s) ────────────────────────────────
$openedAt = isset($body['_t']) ? (int)$body['_t'] : 0;
if ($openedAt === 0 || (time() * 1000 - $openedAt) < 3000) {
wpReject(400, 'Too fast');
}
// ── Rate limiting (5 req / hour / IP) ─────────────────────
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$cacheKey = 'wp-dl-' . md5($ip);
$cache = kirby()->cache('pages');
$hits = (int)($cache->get($cacheKey) ?? 0);
if ($hits >= 5) {
wpReject(429, 'Too many requests');
}
$cache->set($cacheKey, $hits + 1, 60); // TTL 60 min
// ── Validation des champs requis ──────────────────────────
$firstName = trim($body['firstName'] ?? '');
$lastName = trim($body['lastName'] ?? '');
$email = trim($body['email'] ?? '');
if ($firstName === '' || $lastName === '' || $email === '') {
wpReject(422, 'Missing required fields');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
wpReject(422, 'Invalid email');
}
// ── Stocker le lead dans contactDatabase ──────────────────
$company = trim($body['company'] ?? '');
$role = trim($body['role'] ?? '');
$entries = $page->contactDatabase()->toStructure()->toArray();
$existingIndex = null;
foreach ($entries as $i => $entry) {
if (strtolower($entry['email'] ?? '') === strtolower($email)) {
$existingIndex = $i;
break;
}
}
if ($existingIndex !== null) {
// Contact déjà présent — on enrichit les champs vides uniquement
if ($company !== '' && empty($entries[$existingIndex]['company'])) {
$entries[$existingIndex]['company'] = $company;
}
if ($role !== '' && empty($entries[$existingIndex]['role'])) {
$entries[$existingIndex]['role'] = $role;
}
} else {
$entries[] = [
'firstName' => $firstName,
'lastName' => $lastName,
'email' => $email,
'company' => $company,
'role' => $role,
'downloadedAt' => date('d/m/Y H:i'),
];
}
kirby()->impersonate('kirby', function () use ($page, $entries) {
$page->update(['contactDatabase' => \Kirby\Data\Data::encode($entries, 'yaml')]);
});
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'fileUrl' => $page->downloadFile()->toFile()?->url(),
]);
exit;
}
];