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:
parent
d12c31641f
commit
dbba73ea62
3 changed files with 164 additions and 73 deletions
|
|
@ -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'],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,4 +2,9 @@
|
|||
|
||||
return [
|
||||
'debug' => false,
|
||||
|
||||
'brevo_whitepaper' => [
|
||||
'api_key' => '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',
|
||||
'list_id' => 5,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue