$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("Listmonk curl error: {$curlError}");
return false;
}
if ($httpCode < 200 || $httpCode >= 300) {
error_log("Listmonk subscription failed: HTTP {$httpCode}, URL: {$url}, Response: {$response}");
return false;
}
error_log("Listmonk subscription success for {$email}");
return true;
}
/**
* Check IP-based rate limiting to prevent email bombing and spam
* Returns true if IP is allowed, false if rate limit exceeded
*/
function petitionCheckIPRateLimit(string $petitionId, int $maxAttempts = 3, int $windowSeconds = 300): bool {
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$ipHash = hash('sha256', $ipAddress . $petitionId);
$rateLimitFile = dirname(__DIR__, 2) . '/data/petition-rate-limit.csv';
$dir = dirname($rateLimitFile);
if (!is_dir($dir)) {
mkdir($dir, 0750, true);
}
$currentTime = time();
$cutoffTime = $currentTime - $windowSeconds;
// Create file if it doesn't exist
if (!file_exists($rateLimitFile)) {
touch($rateLimitFile);
chmod($rateLimitFile, 0644);
}
$fp = fopen($rateLimitFile, 'a+');
if (!$fp) {
// If we can't open file, allow request (fail open for availability)
return true;
}
if (!flock($fp, LOCK_EX)) {
fclose($fp);
return true;
}
// Read existing attempts
rewind($fp);
$attempts = [];
$otherEntries = [];
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (!isset($row[0]) || !isset($row[1])) {
continue;
}
$timestamp = (int)$row[0];
$storedIpHash = $row[1];
// Clean up old entries (older than window)
if ($timestamp < $cutoffTime) {
continue;
}
// Separate our IP's attempts from others
if ($storedIpHash === $ipHash) {
$attempts[] = $timestamp;
} else {
$otherEntries[] = $row;
}
}
// Check if rate limit exceeded
$recentAttempts = count($attempts);
if ($recentAttempts >= $maxAttempts) {
flock($fp, LOCK_UN);
fclose($fp);
return false;
}
// Add current attempt
$attempts[] = $currentTime;
// Rewrite file with cleaned data
rewind($fp);
ftruncate($fp, 0);
// Write back other IPs' recent attempts
foreach ($otherEntries as $entry) {
fputcsv($fp, $entry, ',', '"', '');
}
// Write our attempts
foreach ($attempts as $timestamp) {
fputcsv($fp, [$timestamp, $ipHash], ',', '"', '');
}
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
/**
* Get translation from language files with section support
*/
function petitionT(Context $ctx, string $section, string $key, array $replacements = []): string {
$translations = $ctx->get('translations', []);
$fullKey = "{$section}.{$key}";
$text = $translations[$fullKey] ?? $key;
foreach ($replacements as $placeholder => $value) {
$text = str_replace("{{$placeholder}}", $value, $text);
}
return $text;
}
/**
* Get regions from language file
*/
function petitionGetRegions(Context $ctx): array {
$translations = $ctx->get('translations', []);
$regions = [];
$regionKeys = [
'agder', 'akershus', 'buskerud', 'finnmark', 'innlandet',
'more_og_romsdal', 'nordland', 'oslo', 'rogaland', 'telemark',
'troms', 'trondelag', 'vestfold', 'vestland', 'ostfold'
];
foreach ($regionKeys as $key) {
$fullKey = "regions.{$key}";
$regions[$key] = $translations[$fullKey] ?? $key;
}
return $regions;
}
/**
* Get path to petition CSV file
*/
function petitionGetCsvPath(string $petitionId): string {
// Sanitize petition ID
$petitionId = preg_replace('/[^a-z0-9\-_]/i', '', $petitionId);
// Additional path traversal protection
if (empty($petitionId) || strpos($petitionId, '..') !== false || strpos($petitionId, '/') !== false || strpos($petitionId, '\\') !== false) {
throw new Exception('Invalid petition ID');
}
return dirname(__DIR__, 2) . "/data/petitions/{$petitionId}.csv";
}
/**
* Extract petition ID from the resolved page directory path
* Uses the last directory segment (the petition's slug) as the ID
*/
function petitionGetIdFromPath(string $pageDir): ?string {
// Get the last directory segment as the petition ID
$petitionSlug = basename($pageDir);
// Sanitize: remove any leading numeric prefix (e.g., "01-" becomes just the slug part)
// This handles folders like "01-my-petition" -> "my-petition"
$petitionSlug = preg_replace('/^\d+-/', '', $petitionSlug);
if (empty($petitionSlug)) {
return null;
}
return $petitionSlug;
}
/**
* Append a new signature to the CSV file with file locking
* Now includes duplicate email check inside the lock to prevent race conditions
*/
function petitionAppendSignature(string $csvPath, array $data): bool {
$dir = dirname($csvPath);
if (!is_dir($dir)) {
mkdir($dir, 0750, true);
}
$isNewFile = !file_exists($csvPath);
// Open for reading and appending
$fp = fopen($csvPath, 'a+');
if (!$fp) {
return false;
}
if (flock($fp, LOCK_EX)) {
// Check for duplicate email INSIDE the lock to prevent race conditions
rewind($fp);
$foundDuplicate = false;
if (!$isNewFile) {
fgetcsv($fp, null, ',', '"', ''); // Skip header
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[1]) && strtolower($row[1]) === strtolower($data['email'])) {
$foundDuplicate = true;
break;
}
}
}
if ($foundDuplicate) {
flock($fp, LOCK_UN);
fclose($fp);
return false;
}
// Write header if new file
if ($isNewFile) {
fputcsv($fp, ['timestamp', 'email', 'firstname', 'surname', 'region', 'display', 'status', 'token', 'token_created', 'ip_hash'], ',', '"', '');
}
// Sanitize user input to prevent CSV injection
fputcsv($fp, [
$data['timestamp'],
petitionSanitizeCSV($data['email']),
petitionSanitizeCSV($data['firstname']),
petitionSanitizeCSV($data['surname']),
$data['region'],
$data['display'],
$data['status'],
$data['token'],
$data['token_created'],
$data['ip_hash']
], ',', '"', '');
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
fclose($fp);
return false;
}
/**
* Update signature status (for confirmation)
* Includes 30-day token expiry check
*/
function petitionConfirmSignature(string $csvPath, string $token): string {
if (!file_exists($csvPath)) {
return 'error';
}
$fp = fopen($csvPath, 'r+');
if (!$fp) {
return 'error';
}
if (!flock($fp, LOCK_EX)) {
fclose($fp);
return 'error';
}
$rows = [];
$header = fgetcsv($fp, null, ',', '"', '');
$rows[] = $header;
$found = false;
$alreadyConfirmed = false;
$tokenExpired = false;
$currentTime = time();
// Token is at index 7, token_created is at index 8
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[7]) && $row[7] === $token) {
$found = true;
// Check if token is expired (30 days = 2592000 seconds)
$tokenCreated = isset($row[8]) ? (int)$row[8] : 0;
if ($tokenCreated > 0 && ($currentTime - $tokenCreated) > 2592000) {
$tokenExpired = true;
} elseif ($row[6] === 'confirmed') {
$alreadyConfirmed = true;
} else {
$row[6] = 'confirmed';
}
}
$rows[] = $row;
}
if (!$found) {
flock($fp, LOCK_UN);
fclose($fp);
return 'error';
}
if ($tokenExpired) {
flock($fp, LOCK_UN);
fclose($fp);
return 'expired';
}
if ($alreadyConfirmed) {
flock($fp, LOCK_UN);
fclose($fp);
return 'already';
}
// Rewrite file
rewind($fp);
ftruncate($fp, 0);
foreach ($rows as $row) {
fputcsv($fp, $row, ',', '"', '');
}
flock($fp, LOCK_UN);
fclose($fp);
return 'success';
}
/**
* Delete a signature by token (for GDPR compliance)
*/
function petitionDeleteSignature(string $csvPath, string $token): string {
if (!file_exists($csvPath)) {
return 'error';
}
$fp = fopen($csvPath, 'r+');
if (!$fp) {
return 'error';
}
if (!flock($fp, LOCK_EX)) {
fclose($fp);
return 'error';
}
$rows = [];
$header = fgetcsv($fp, null, ',', '"', '');
$rows[] = $header;
$found = false;
// Token is at index 7
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[7]) && $row[7] === $token) {
$found = true;
// Skip this row (don't add to $rows) to delete it
continue;
}
$rows[] = $row;
}
if (!$found) {
flock($fp, LOCK_UN);
fclose($fp);
return 'error';
}
// Rewrite file without the deleted row
rewind($fp);
ftruncate($fp, 0);
foreach ($rows as $row) {
fputcsv($fp, $row, ',', '"', '');
}
flock($fp, LOCK_UN);
fclose($fp);
return 'success';
}
/**
* Check if email already exists in petition
*/
function petitionEmailExists(string $csvPath, string $email): bool {
if (!file_exists($csvPath)) {
return false;
}
$fp = fopen($csvPath, 'r');
if (!$fp) {
return false;
}
if (flock($fp, LOCK_SH)) {
fgetcsv($fp, null, ',', '"', ''); // Skip header
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[1]) && strtolower($row[1]) === strtolower($email)) {
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
}
flock($fp, LOCK_UN);
}
fclose($fp);
return false;
}
/**
* Get all confirmed signatures
*/
function petitionGetConfirmedSignatures(string $csvPath): array {
if (!file_exists($csvPath)) {
return [];
}
$signatures = [];
$fp = fopen($csvPath, 'r');
if (!$fp) {
return [];
}
if (flock($fp, LOCK_SH)) {
fgetcsv($fp, null, ',', '"', ''); // Skip header
// CSV format: timestamp, email, firstname, surname, region, display, status, token, ip_hash
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[6]) && $row[6] === 'confirmed') {
$signatures[] = [
'timestamp' => (int)$row[0],
'firstname' => $row[2],
'surname' => $row[3],
'region' => $row[4],
'display' => $row[5]
];
}
}
flock($fp, LOCK_UN);
}
fclose($fp);
// Sort newest first
usort($signatures, fn($a, $b) => $b['timestamp'] - $a['timestamp']);
return $signatures;
}
/**
* Format signature for display based on privacy preference
*/
function petitionFormatSignature(array $signature, Context $ctx): string {
$regions = petitionGetRegions($ctx);
$regionDisplay = $regions[$signature['region']] ?? $signature['region'];
$fromLabel = petitionT($ctx, 'petition', 'from_region');
switch ($signature['display']) {
case 'anonymous':
$anonLabel = petitionT($ctx, 'petition', 'anonymous_name');
return $anonLabel . ' ' . $fromLabel . ' ' . $regionDisplay;
case 'full':
$fullName = htmlspecialchars($signature['firstname'] . ' ' . $signature['surname']);
return $fullName . ' ' . $fromLabel . ' ' . $regionDisplay;
case 'semi':
default:
return htmlspecialchars($signature['firstname']) . ' ' . $fromLabel . ' ' . $regionDisplay;
}
}
/**
* Format timestamp for display
*/
function petitionFormatDate(int $timestamp, Context $ctx): string {
$currentLang = $ctx->get('currentLang', 'no');
$months = [
'no' => ['januar', 'februar', 'mars', 'april', 'mai', 'juni',
'juli', 'august', 'september', 'oktober', 'november', 'desember'],
'en' => ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December']
];
$monthNames = $months[$currentLang] ?? $months['no'];
$day = date('j', $timestamp);
$month = $monthNames[(int)date('n', $timestamp) - 1];
$year = date('Y', $timestamp);
$time = date('H:i', $timestamp);
if ($currentLang === 'en') {
return "{$month} {$day}, {$year}, {$time}";
}
return "{$day}. {$month} {$year}, {$time}";
}
/**
* Log email send attempt to SMTP log file
* Used for tracking failed sends and enabling retry via CLI
*/
function petitionLogEmail(string $type, string $petitionId, string $email, bool $success, string $errorMessage = ''): void {
$logPath = dirname(__DIR__, 2) . '/data/smtp-log.csv';
$dir = dirname($logPath);
if (!is_dir($dir)) {
mkdir($dir, 0750, true);
}
$isNewFile = !file_exists($logPath);
$fp = fopen($logPath, 'a');
if (!$fp) {
return;
}
if (flock($fp, LOCK_EX)) {
if ($isNewFile) {
fputcsv($fp, ['timestamp', 'type', 'petition_id', 'email', 'status', 'error_message'], ',', '"', '');
}
fputcsv($fp, [
time(),
$type,
$petitionId,
$email,
$success ? 'success' : 'failed',
$errorMessage
], ',', '"', '');
flock($fp, LOCK_UN);
}
fclose($fp);
}
/**
* Retry wrapper for email sending with exponential backoff
* Retries up to maxRetries times with increasing delays
*/
function petitionSendWithRetry(callable $sendFunction, int $maxRetries = 3): bool {
$delays = [2, 4, 8]; // Exponential backoff: 2s, 4s, 8s
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
$result = $sendFunction();
if ($result === true) {
if ($attempt > 1) {
// Log successful retry (no sensitive data)
error_log("Petition email succeeded on attempt {$attempt}/{$maxRetries}");
}
return true;
}
// If not the last attempt, wait before retrying
if ($attempt < $maxRetries) {
$delay = $delays[$attempt - 1] ?? 8;
sleep($delay);
}
}
// All retries failed
error_log("Petition email failed after {$maxRetries} attempts");
return false;
}
/**
* Send confirmation email (with retry wrapper)
*/
function petitionSendConfirmationEmail(array $data, string $confirmUrl, string $petitionTitle, string $petitionId, Context $ctx): bool {
$result = petitionSendWithRetry(function() use ($data, $confirmUrl, $petitionTitle, $ctx, &$errorMessage) {
return petitionSendConfirmationEmailInternal($data, $confirmUrl, $petitionTitle, $ctx, $errorMessage);
});
// Log the final result
petitionLogEmail('confirmation', $petitionId, $data['email'], $result, $result ? '' : ($errorMessage ?? 'Unknown error'));
return $result;
}
/**
* Internal: Send confirmation email (single attempt)
*/
function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl, string $petitionTitle, Context $ctx, ?string &$errorMessage = null): bool {
// Decode HTML entities for plain-text email (e.g., – → –)
$petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php';
if (!file_exists($smtpConfigPath)) {
$errorMessage = 'SMTP config not found';
return false;
}
$config = require $smtpConfigPath;
if (!$config['enabled']) {
$errorMessage = 'SMTP disabled';
return false;
}
// Get petition-specific overrides
$fromEmail = $config['petition']['from_email'] ?? $config['from_email'];
$fromName = $config['petition']['from_name'] ?? $config['from_name'];
// Build email body
$greeting = petitionT($ctx, 'petition', 'email_greeting', ['name' => $data['firstname']]);
$thanks = petitionT($ctx, 'petition', 'email_thanks', ['title' => $petitionTitle]);
$confirmText = petitionT($ctx, 'petition', 'email_confirm');
$expiryNotice = petitionT($ctx, 'petition', 'email_expiry_notice');
$ignoreText = petitionT($ctx, 'petition', 'email_ignore');
$rightsInfo = petitionT($ctx, 'petition', 'email_rights_info');
$signature = petitionT($ctx, 'petition', 'email_signature');
$org = petitionT($ctx, 'petition', 'email_org');
$emailBody = "{$greeting}\n\n";
$emailBody .= "{$thanks}\n\n";
$emailBody .= "{$confirmText}\n";
$emailBody .= "{$confirmUrl}\n\n";
$emailBody .= "{$expiryNotice}\n\n";
$emailBody .= "{$ignoreText}\n\n";
$emailBody .= "---\n\n";
$emailBody .= "{$rightsInfo}\n\n";
$emailBody .= "{$signature},\n";
$emailBody .= "{$org}\n";
$subject = petitionT($ctx, 'petition', 'email_subject');
// Get petition-specific SMTP settings (allows separate SMTP account for better deliverability)
$smtpHost = $config['petition']['host'] ?? $config['host'];
$smtpPort = $config['petition']['port'] ?? $config['port'];
$smtpUser = $config['petition']['username'] ?? $config['username'];
$smtpPass = $config['petition']['password'] ?? $config['password'];
// Pre-flight check
$fp = @fsockopen($smtpHost, $smtpPort, $errno, $errstr, 10);
if (!$fp) {
$errorMessage = "Connection failed: {$errno} - {$errstr}";
error_log("Petition SMTP pre-flight failed: {$smtpHost}:{$smtpPort} - {$errno} - {$errstr}");
return false;
}
fclose($fp);
try {
require_once dirname(__DIR__, 2) . '/vendor/PHPMailer.Lite.php';
$mail = new \codeworxtech\PHPMailerLite\PHPMailerLite();
$mail->SetSMTPhost($smtpHost);
$mail->SetSMTPport($smtpPort);
$mail->SetSMTPuser($smtpUser);
$mail->SetSMTPpass($smtpPass);
$recipientName = $data['firstname'] . ' ' . $data['surname'];
$mail->SetSender([$fromEmail => $fromName]);
$mail->AddRecipient([$data['email'] => $recipientName]);
$mail->SetSubject($subject);
$mail->SetBodyText($emailBody);
ob_start();
$sent = @$mail->Send('smtp');
$output = ob_get_clean();
if (!$sent || stripos($output, 'error') !== false || stripos($output, '✗') !== false) {
$errorMessage = strip_tags($output) ?: 'Send returned false';
error_log("Petition email send failed: " . strip_tags($output));
return false;
}
return true;
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
error_log("Petition email exception: " . $e->getMessage());
return false;
}
}
/**
* Get signature data by token
*/
function petitionGetSignatureByToken(string $csvPath, string $token): ?array {
if (!file_exists($csvPath)) {
return null;
}
$fp = fopen($csvPath, 'r');
if (!$fp) {
return null;
}
$signature = null;
if (flock($fp, LOCK_SH)) {
fgetcsv($fp, null, ',', '"', ''); // Skip header
// CSV format: timestamp, email, firstname, surname, region, display, status, token, ip_hash
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[7]) && $row[7] === $token) {
$signature = [
'email' => $row[1],
'firstname' => $row[2],
'surname' => $row[3]
];
break;
}
}
flock($fp, LOCK_UN);
}
fclose($fp);
return $signature;
}
/**
* Get pending signature data by email address
* Returns signature data if found and pending, null otherwise
*/
function petitionGetPendingSignatureByEmail(string $csvPath, string $email): ?array {
if (!file_exists($csvPath)) {
return null;
}
$fp = fopen($csvPath, 'r');
if (!$fp) {
return null;
}
$signature = null;
$email = strtolower(trim($email));
if (flock($fp, LOCK_SH)) {
fgetcsv($fp, null, ',', '"', ''); // Skip header
// CSV format: timestamp, email, firstname, surname, region, display, status, token, token_created, ip_hash
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[1]) && strtolower($row[1]) === $email) {
$signature = [
'email' => $row[1],
'firstname' => $row[2],
'surname' => $row[3],
'region' => $row[4],
'display' => $row[5],
'status' => $row[6],
'token' => $row[7],
'token_created' => $row[8] ?? 0
];
break;
}
}
flock($fp, LOCK_UN);
}
fclose($fp);
return $signature;
}
/**
* Update token for an existing signature (for resend functionality)
*/
function petitionUpdateSignatureToken(string $csvPath, string $email, string $newToken): bool {
if (!file_exists($csvPath)) {
return false;
}
$fp = fopen($csvPath, 'r+');
if (!$fp) {
return false;
}
if (!flock($fp, LOCK_EX)) {
fclose($fp);
return false;
}
$rows = [];
$header = fgetcsv($fp, null, ',', '"', '');
$rows[] = $header;
$found = false;
$email = strtolower(trim($email));
$currentTime = time();
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[1]) && strtolower($row[1]) === $email && $row[6] === 'pending') {
$row[7] = $newToken; // Update token
$row[8] = $currentTime; // Update token_created
$found = true;
}
$rows[] = $row;
}
if (!$found) {
flock($fp, LOCK_UN);
fclose($fp);
return false;
}
// Rewrite file
rewind($fp);
ftruncate($fp, 0);
foreach ($rows as $row) {
fputcsv($fp, $row, ',', '"', '');
}
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
/**
* Send thank you email with delete link (with retry wrapper)
*/
function petitionSendThankYouEmail(string $token, string $deleteUrl, string $petitionTitle, string $petitionId, string $csvPath, Context $ctx): bool {
$signature = petitionGetSignatureByToken($csvPath, $token);
$email = $signature['email'] ?? 'unknown';
$result = petitionSendWithRetry(function() use ($token, $deleteUrl, $petitionTitle, $csvPath, $ctx, &$errorMessage) {
return petitionSendThankYouEmailInternal($token, $deleteUrl, $petitionTitle, $csvPath, $ctx, $errorMessage);
});
// Log the final result
petitionLogEmail('thankyou', $petitionId, $email, $result, $result ? '' : ($errorMessage ?? 'Unknown error'));
return $result;
}
/**
* Internal: Send thank you email with delete link (single attempt)
*/
function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, string $petitionTitle, string $csvPath, Context $ctx, ?string &$errorMessage = null): bool {
// Decode HTML entities for plain-text email (e.g., – → –)
$petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$signature = petitionGetSignatureByToken($csvPath, $token);
if (!$signature) {
$errorMessage = 'Signature not found';
return false;
}
$smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php';
if (!file_exists($smtpConfigPath)) {
$errorMessage = 'SMTP config not found';
return false;
}
$config = require $smtpConfigPath;
if (!$config['enabled']) {
$errorMessage = 'SMTP disabled';
return false;
}
// Get petition-specific overrides
$fromEmail = $config['petition']['from_email'] ?? $config['from_email'];
$fromName = $config['petition']['from_name'] ?? $config['from_name'];
// Build email body
$greeting = petitionT($ctx, 'petition', 'email_greeting', ['name' => $signature['firstname']]);
$thankYouText = petitionT($ctx, 'petition', 'email_thankyou_confirmed', ['title' => $petitionTitle]);
$deleteText = petitionT($ctx, 'petition', 'email_delete_info');
$rightsInfo = petitionT($ctx, 'petition', 'email_rights_info');
$emailSignature = petitionT($ctx, 'petition', 'email_signature');
$org = petitionT($ctx, 'petition', 'email_org');
$emailBody = "{$greeting}\n\n";
$emailBody .= "{$thankYouText}\n\n";
$emailBody .= "{$deleteText}\n";
$emailBody .= "{$deleteUrl}\n\n";
$emailBody .= "---\n\n";
$emailBody .= "{$rightsInfo}\n\n";
$emailBody .= "{$emailSignature},\n";
$emailBody .= "{$org}\n";
$subject = petitionT($ctx, 'petition', 'email_thankyou_subject');
// Get petition-specific SMTP settings (allows separate SMTP account for better deliverability)
$smtpHost = $config['petition']['host'] ?? $config['host'];
$smtpPort = $config['petition']['port'] ?? $config['port'];
$smtpUser = $config['petition']['username'] ?? $config['username'];
$smtpPass = $config['petition']['password'] ?? $config['password'];
// Pre-flight check
$fp = @fsockopen($smtpHost, $smtpPort, $errno, $errstr, 10);
if (!$fp) {
$errorMessage = "Connection failed: {$errno} - {$errstr}";
error_log("Petition SMTP pre-flight failed: {$smtpHost}:{$smtpPort} - {$errno} - {$errstr}");
return false;
}
fclose($fp);
try {
require_once dirname(__DIR__, 2) . '/vendor/PHPMailer.Lite.php';
$mail = new \codeworxtech\PHPMailerLite\PHPMailerLite();
$mail->SetSMTPhost($smtpHost);
$mail->SetSMTPport($smtpPort);
$mail->SetSMTPuser($smtpUser);
$mail->SetSMTPpass($smtpPass);
$recipientName = $signature['firstname'] . ' ' . $signature['surname'];
$mail->SetSender([$fromEmail => $fromName]);
$mail->AddRecipient([$signature['email'] => $recipientName]);
$mail->SetSubject($subject);
$mail->SetBodyText($emailBody);
ob_start();
$sent = @$mail->Send('smtp');
$output = ob_get_clean();
if (!$sent || stripos($output, 'error') !== false || stripos($output, '✗') !== false) {
$errorMessage = strip_tags($output) ?: 'Send returned false';
error_log("Petition thank you email send failed: " . strip_tags($output));
return false;
}
return true;
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
error_log("Petition thank you email exception: " . $e->getMessage());
return false;
}
}
/**
* Render the petition form HTML
*/
function petitionRenderForm(Context $ctx, array $formData, array $errors, bool $showForm): string {
$csrfToken = $_SESSION['csrf_token'];
$currentTime = time();
$regions = petitionGetRegions($ctx);
$html = ' ' . htmlspecialchars(petitionT($ctx, 'petition', 'newest_first')) . '' . htmlspecialchars(petitionT($ctx, 'petition', 'form_title')) . '
';
// Error messages
if (!empty($errors)) {
$html .= '';
foreach ($errors as $error) {
$html .= '
';
$html .= '' . htmlspecialchars(petitionT($ctx, 'petition', 'signatures_title')) . ' (' . $count . ')
';
if ($count > 0) {
$html .= '';
foreach ($signatures as $sig) {
$html .= '
';
}
$html .= '