All checks were successful
Deploy Production / Deploy to Production (push) Successful in 32s
- 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>
244 lines
7.9 KiB
PHP
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;
|
|
},
|
|
];
|