* = newsletter_signup('Short text here', 'small') ?> * * Available themes: * - 'hero': Full-width gradient background section (like CTA sections) * - 'small': Compact inline notice with rounded border, fits between paragraphs */ // Start session if not already started if (session_status() === PHP_SESSION_NONE) { session_start(); } // Generate CSRF token if not exists if (empty($_SESSION['newsletter_csrf_token'])) { $_SESSION['newsletter_csrf_token'] = bin2hex(random_bytes(32)); } /** * Get translation for newsletter plugin */ function newsletterT(string $key): string { $ctx = $GLOBALS['ctx'] ?? null; if (!$ctx) { // Fallback values if no context $fallbacks = [ 'name_label' => 'Navn', 'name_placeholder' => 'Ditt navn', 'email_label' => 'E-post', 'email_placeholder' => 'Din e-postadresse', 'notice' => 'Vi sender ca. 12 e-poster i året.', 'submit_button' => 'Meld deg på', 'success_message' => 'Sjekk innboksen din!', 'error_message' => 'Noe gikk galt!' ]; return $fallbacks[$key] ?? $key; } $translations = $ctx->get('translations', []); $fullKey = "newsletter.{$key}"; return $translations[$fullKey] ?? $key; } /** * Subscribe to newsletter via Listmonk public API */ function newsletterSubscribe(string $email, string $name): array { $configPath = dirname(__DIR__, 2) . '/listmonk-config.php'; if (!file_exists($configPath)) { return ['success' => false, 'error' => 'config_missing']; } $config = require $configPath; if (!$config['enabled']) { return ['success' => false, 'error' => 'disabled']; } $payload = json_encode([ 'email' => $email, 'name' => $name, 'list_uuids' => $config['list_uuids'] ]); $url = $config['url'] . '/api/public/subscription'; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10 ]); $response = curl_exec($ch); $curlError = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($curlError) { error_log("Newsletter curl error: {$curlError}"); return ['success' => false, 'error' => 'network']; } if ($httpCode >= 200 && $httpCode < 300) { return ['success' => true]; } error_log("Newsletter subscription failed: HTTP {$httpCode}, Response: {$response}"); return ['success' => false, 'error' => 'api']; } /** * Rate limit check for newsletter signups */ function newsletterCheckRateLimit(): bool { $lastSubmit = $_SESSION['newsletter_last_submit'] ?? 0; return (time() - $lastSubmit) >= 30; } /** * Get shared form translations */ function newsletterGetTranslations(): array { return [ 'nameLabel' => htmlspecialchars(newsletterT('name_label'), ENT_QUOTES, 'UTF-8'), 'namePlaceholder' => htmlspecialchars(newsletterT('name_placeholder'), ENT_QUOTES, 'UTF-8'), 'emailLabel' => htmlspecialchars(newsletterT('email_label'), ENT_QUOTES, 'UTF-8'), 'emailPlaceholder' => htmlspecialchars(newsletterT('email_placeholder'), ENT_QUOTES, 'UTF-8'), 'notice' => htmlspecialchars(newsletterT('notice'), ENT_QUOTES, 'UTF-8'), 'submitButton' => htmlspecialchars(newsletterT('submit_button'), ENT_QUOTES, 'UTF-8'), 'successMessage' => htmlspecialchars(newsletterT('success_message'), ENT_QUOTES, 'UTF-8'), 'errorMessage' => htmlspecialchars(newsletterT('error_message'), ENT_QUOTES, 'UTF-8'), ]; } /** * Get shared JavaScript (only included once per page) */ function newsletterGetScript(): string { static $scriptIncluded = false; if ($scriptIncluded) { return ''; } $scriptIncluded = true; return <<<'SCRIPT' SCRIPT; } /** * Render the "hero" theme - full-width gradient background section */ function newsletterRenderHero(string $introText, string $formId): string { $csrfToken = $_SESSION['newsletter_csrf_token']; $nonce = bin2hex(random_bytes(8)); $escapedIntro = htmlspecialchars($introText, ENT_QUOTES, 'UTF-8'); $escapedFormId = htmlspecialchars($formId, ENT_QUOTES, 'UTF-8'); $t = newsletterGetTranslations(); $html = <<
HTML; return $html; } /** * Render the "small" theme - compact inline notice with border */ function newsletterRenderSmall(string $introText, string $formId): string { $csrfToken = $_SESSION['newsletter_csrf_token']; $nonce = bin2hex(random_bytes(8)); $escapedIntro = htmlspecialchars($introText, ENT_QUOTES, 'UTF-8'); $escapedFormId = htmlspecialchars($formId, ENT_QUOTES, 'UTF-8'); $t = newsletterGetTranslations(); $html = << HTML; return $html; } /** * Get CSS for all themes (only included once per page) */ function newsletterGetStyles(): string { static $stylesIncluded = false; if ($stylesIncluded) { return ''; } $stylesIncluded = true; return <<<'STYLES' STYLES; } /** * Render the newsletter signup section HTML * This is the main function to call from content files. * * @param string $introText Custom intro text to display above the form * @param string $theme Theme to use: 'hero' (default) or 'small' * @param string $formId Optional form ID for the section (default: 'newsletter') * @return string The complete HTML for the newsletter signup section */ function newsletter_signup(string $introText, string $theme = 'hero', string $formId = 'newsletter'): string { $html = newsletterGetStyles(); switch ($theme) { case 'small': $html .= newsletterRenderSmall($introText, $formId); break; case 'hero': default: $html .= newsletterRenderHero($introText, $formId); break; } $html .= newsletterGetScript(); return $html; } /** * Handle AJAX form submission */ function newsletterHandleSubmission(): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['newsletter_submit'])) { return; } header('Content-Type: application/json'); // CSRF validation if (!isset($_POST['newsletter_csrf']) || !hash_equals($_SESSION['newsletter_csrf_token'] ?? '', $_POST['newsletter_csrf'])) { echo json_encode(['success' => false, 'error' => 'csrf']); exit; } // Rate limit if (!newsletterCheckRateLimit()) { echo json_encode(['success' => false, 'error' => 'rate_limit']); exit; } // Validate input $name = trim($_POST['newsletter_name'] ?? ''); $email = strtolower(trim($_POST['newsletter_email'] ?? '')); if (empty($name) || strlen($name) < 2 || strlen($name) > 100) { echo json_encode(['success' => false, 'error' => 'invalid_name']); exit; } if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 100) { echo json_encode(['success' => false, 'error' => 'invalid_email']); exit; } // Subscribe $result = newsletterSubscribe($email, $name); if ($result['success']) { $_SESSION['newsletter_last_submit'] = time(); } echo json_encode($result); exit; } // Handle form submission early (before any output) newsletterHandleSubmission();