feat: connect white paper download form to Brevo

Refactors the download route into clean single-responsibility functions
and adds Brevo contact sync (list ID configurable via brevo_whitepaper.list_id).
Includes a stub for future external email deliverability validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-06-04 07:37:32 +02:00
parent d12c31641f
commit dbba73ea62
3 changed files with 164 additions and 73 deletions

View file

@ -12,6 +12,11 @@ return [
'thumbs' => require __DIR__ . '/thumbs.php',
'brevo_whitepaper' => [
'api_key' => '', // leave empty in dev to skip Brevo sync
'list_id' => 5,
],
'cache' => [
'redirects' => ['type' => 'file'],
],

View file

@ -2,4 +2,9 @@
return [
'debug' => false,
'brevo_whitepaper' => [
'api_key' => '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',
'list_id' => 5,
],
];

View file

@ -1,12 +1,151 @@
<?php
function wpReject(int $code, string $message): void {
// ── Helpers ───────────────────────────────────────────────────────────────────
function wpReject(int $code, string $message): never
{
http_response_code($code);
header('Content-Type: application/json');
echo json_encode(['error' => $message]);
exit;
}
function isHoneypotFilled(array $body): bool
{
return !empty($body['_hp']);
}
function checkRateLimit(string $ip): void
{
$maxHits = 5;
$ttlMinutes = 60;
$cache = kirby()->cache('pages');
$cacheKey = 'wp-dl-' . md5($ip);
$hits = (int)($cache->get($cacheKey) ?? 0);
if ($hits >= $maxHits) {
wpReject(429, 'Too many requests');
}
$cache->set($cacheKey, $hits + 1, $ttlMinutes);
}
function parseContact(array $body): array
{
$firstName = trim($body['firstName'] ?? '');
$lastName = trim($body['lastName'] ?? '');
$email = trim($body['email'] ?? '');
$company = trim($body['company'] ?? '');
$role = trim($body['role'] ?? '');
if ($firstName === '' || $lastName === '' || $email === '') {
wpReject(422, 'Missing required fields');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
wpReject(422, 'Invalid email format');
}
return compact('firstName', 'lastName', 'email', 'company', 'role');
}
function checkEmailDeliverability(string $email): bool
{
// Plug in an external email validation API here (e.g. Hunter, ZeroBounce, AbstractAPI)
return true;
}
function upsertKirbyContact(\Kirby\Cms\Page $page, array $contact): void
{
$entries = page('livres-blancs')->contactDatabase()->toStructure()->toArray();
$pageUuid = $page->uuid()->toString();
$existingIndex = null;
foreach ($entries as $i => $entry) {
if (strtolower($entry['email'] ?? '') === strtolower($contact['email'])) {
$existingIndex = $i;
break;
}
}
if ($existingIndex !== null) {
if ($contact['company'] !== '' && empty($entries[$existingIndex]['company'])) {
$entries[$existingIndex]['company'] = $contact['company'];
}
if ($contact['role'] !== '' && empty($entries[$existingIndex]['role'])) {
$entries[$existingIndex]['role'] = $contact['role'];
}
$whitePapers = $entries[$existingIndex]['whitePaper'] ?? [];
if (!is_array($whitePapers)) {
$whitePapers = array_filter(array_map('trim', explode(',', (string) $whitePapers)));
}
if (!in_array($pageUuid, $whitePapers, true)) {
$whitePapers[] = $pageUuid;
}
unset($entries[$existingIndex]['whitepaper']);
$entries[$existingIndex]['whitePaper'] = $whitePapers;
} else {
$entries[] = [
'firstName' => $contact['firstName'],
'lastName' => $contact['lastName'],
'email' => $contact['email'],
'company' => $contact['company'],
'role' => $contact['role'],
'whitePaper' => [$pageUuid],
'downloadedAt' => date('d/m/Y H:i'),
];
}
$defaultLang = kirby()->defaultLanguage()->code();
kirby()->impersonate('kirby', function () use ($entries, $defaultLang) {
page('livres-blancs')->update(
['contactDatabase' => \Kirby\Data\Data::encode($entries, 'yaml')],
$defaultLang
);
});
}
function upsertBrevoContact(array $contact, int $listId, string $apiKey): void
{
$payload = json_encode([
'email' => $contact['email'],
'listIds' => [$listId],
'updateEnabled' => true,
'attributes' => [
'FIRSTNAME' => $contact['firstName'],
'LASTNAME' => $contact['lastName'],
'COMPANY' => $contact['company'],
// JOBTITLE is a Brevo built-in; ROLE requires a custom attribute in your Brevo account
'JOBTITLE' => $contact['role'],
],
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.brevo.com/v3/contacts',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'api-key: ' . $apiKey,
],
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || ($httpCode !== 201 && $httpCode !== 204)) {
error_log('[WP] Brevo upsert failed (HTTP ' . $httpCode . '): ' . $response);
}
}
// ── Route ─────────────────────────────────────────────────────────────────────
return [
'pattern' => ['(:any)/(:any)/download', 'en/(:any)/(:any)/download'],
'method' => 'POST',
@ -19,91 +158,33 @@ return [
$body = kirby()->request()->body()->toArray();
// ── Honeypot ──────────────────────────────────────────────
if (!empty($body['_hp'])) {
if (isHoneypotFilled($body)) {
wpReject(400, 'Bad request');
}
// ── Rate limiting (5 req / hour / IP) ─────────────────────
$ip = $_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
checkRateLimit($_SERVER['REMOTE_ADDR'] ?? 'unknown');
// ── Validation des champs requis ──────────────────────────
$firstName = trim($body['firstName'] ?? '');
$lastName = trim($body['lastName'] ?? '');
$email = trim($body['email'] ?? '');
$contact = parseContact($body);
if ($firstName === '' || $lastName === '' || $email === '') {
wpReject(422, 'Missing required fields');
if (!checkEmailDeliverability($contact['email'])) {
wpReject(422, 'Undeliverable email');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
wpReject(422, 'Invalid email');
$brevo = kirby()->option('brevo_whitepaper', []);
$brevoApiKey = $brevo['api_key'] ?? '';
$brevoListId = (int)($brevo['list_id'] ?? 5);
upsertKirbyContact($page, $contact);
if ($brevoApiKey !== '') {
upsertBrevoContact($contact, $brevoListId, $brevoApiKey);
}
// ── Stocker le lead dans contactDatabase ──────────────────
$company = trim($body['company'] ?? '');
$role = trim($body['role'] ?? '');
$entries = page('livres-blancs')->contactDatabase()->toStructure()->toArray();
$existingIndex = null;
foreach ($entries as $i => $entry) {
if (strtolower($entry['email'] ?? '') === strtolower($email)) {
$existingIndex = $i;
break;
}
}
$pageUuid = $page->uuid()->toString();
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;
}
$whitePapers = $entries[$existingIndex]['whitepaper'] ?? [];
if (!is_array($whitePapers)) {
$whitePapers = array_filter(array_map('trim', explode(',', (string) $whitePapers)));
}
if (!in_array($pageUuid, $whitePapers, true)) {
$whitePapers[] = $pageUuid;
}
unset($entries[$existingIndex]['whitepaper']);
$entries[$existingIndex]['whitePaper'] = $whitePapers;
} else {
$entries[] = [
'firstName' => $firstName,
'lastName' => $lastName,
'email' => $email,
'company' => $company,
'role' => $role,
'whitePaper' => [$pageUuid],
'downloadedAt' => date('d/m/Y H:i'),
];
}
$defaultLang = kirby()->defaultLanguage()->code();
kirby()->impersonate('kirby', function () use ($entries, $defaultLang) {
page('livres-blancs')->update(
['contactDatabase' => \Kirby\Data\Data::encode($entries, 'yaml')],
$defaultLang
);
});
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'fileUrl' => $page->downloadFile()->toFile()?->url(),
]);
exit;
}
},
];