world-game/site/config/routes/download-white-paper.php
isUnknown d087012ea8
All checks were successful
Deploy Production / Deploy to Production (push) Successful in 32s
feat: validate email via Emailable, remove auto-download, fix keyboard nav in inputs
- Plug Emailable API into checkEmailDeliverability (fail open if no key)
- Show inline red error + shake animation on email field when address is undeliverable
- Remove window.open auto-download after form submission; success now checks result.success
- Disable arrow-key slide navigation when an input or textarea is focused

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 14:57:03 +02:00

244 lines
7.9 KiB
PHP

<?php
// ── 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, string $apiKey): bool
{
if ($apiKey === '') {
error_log('[WP] Emailable: no api_key configured, skipping email check');
return true;
}
$url = 'https://api.emailable.com/v1/verify?' . http_build_query([
'email' => $email,
'api_key' => $apiKey,
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPGET => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
error_log('[WP] Emailable check failed (HTTP ' . $httpCode . '): ' . $curlError);
return true; // fail open
}
$data = json_decode($response, true);
$state = $data['state'] ?? 'unknown';
error_log('[WP] Emailable response for ' . $email . ': state=' . $state . ' | raw=' . $response);
return $state !== 'undeliverable';
}
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): array
{
$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);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
return ['ok' => false, 'code' => 0, 'error' => 'cURL error: ' . $curlError];
}
$ok = $httpCode === 201 || $httpCode === 204;
if (!$ok) {
$body = json_decode($response, true);
return [
'ok' => false,
'code' => $httpCode,
'error' => $body['message'] ?? $response,
];
}
return ['ok' => true, 'code' => $httpCode];
}
// ── Route ─────────────────────────────────────────────────────────────────────
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();
if (isHoneypotFilled($body)) {
wpReject(400, 'Bad request');
}
checkRateLimit($_SERVER['REMOTE_ADDR'] ?? 'unknown');
$contact = parseContact($body);
$emailableApiKey = kirby()->option('emailable_whitepaper', [])['api_key'] ?? '';
if (!checkEmailDeliverability($contact['email'], $emailableApiKey)) {
wpReject(422, 'Undeliverable email');
}
$brevo = kirby()->option('brevo_whitepaper', []);
$brevoApiKey = $brevo['api_key'] ?? '';
$brevoListId = (int)($brevo['list_id'] ?? 5);
error_log('[WP] hostname: ' . ($_SERVER['SERVER_NAME'] ?? 'unknown') . ' | brevo api_key: "' . $brevoApiKey . '"');
upsertKirbyContact($page, $contact);
$brevoResult = null;
if ($brevoApiKey !== '') {
$brevoResult = upsertBrevoContact($contact, $brevoListId, $brevoApiKey);
if (!$brevoResult['ok']) {
error_log('[WP] Brevo upsert failed (HTTP ' . $brevoResult['code'] . '): ' . ($brevoResult['error'] ?? ''));
}
}
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'fileUrl' => $page->downloadFile()->toFile()?->url(),
'brevo' => $brevoResult,
]);
exit;
},
];