diff --git a/content/04-nyhetsbrev.php b/content/04-nyhetsbrev.php new file mode 100644 index 0000000..4ccdcd9 --- /dev/null +++ b/content/04-nyhetsbrev.php @@ -0,0 +1 @@ += newsletter_signup('Få månedelige nyhetsbrev rett i innboksen!', 'small') ?> diff --git a/custom/plugins/page/newsletter-signup.php b/custom/plugins/page/newsletter-signup.php new file mode 100644 index 0000000..6c3cee2 --- /dev/null +++ b/custom/plugins/page/newsletter-signup.php @@ -0,0 +1,521 @@ + + * = 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();