2026-01-16 00:37:13 +01:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Petition Form Plugin
|
|
|
|
|
|
*
|
|
|
|
|
|
* GDPR-compliant petition system with double opt-in email confirmation.
|
|
|
|
|
|
* File-based storage (CSV) with proper locking to prevent corruption.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Usage: Add to metadata.ini:
|
|
|
|
|
|
* plugins = "petition-form"
|
|
|
|
|
|
* petition_id = "my-petition"
|
|
|
|
|
|
* thank_you_page = "takk" (optional, defaults to "takk")
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
// Start session if not already started
|
|
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
|
|
|
|
session_start();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Generate CSRF token if not exists
|
|
|
|
|
|
if (empty($_SESSION['csrf_token'])) {
|
|
|
|
|
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Helper Functions ---
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Sanitize value to prevent CSV injection attacks
|
|
|
|
|
|
* Prefixes values starting with dangerous characters that could be interpreted as formulas
|
|
|
|
|
|
*/
|
|
|
|
|
|
function petitionSanitizeCSV(string $value): string {
|
|
|
|
|
|
if (empty($value)) {
|
|
|
|
|
|
return $value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$firstChar = substr($value, 0, 1);
|
|
|
|
|
|
$dangerousChars = ['=', '+', '-', '@', "\t", "\r", "\n"];
|
|
|
|
|
|
|
|
|
|
|
|
if (in_array($firstChar, $dangerousChars, true)) {
|
|
|
|
|
|
return "'" . $value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 21:43:48 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Subscribe email to Listmonk newsletter lists via public API
|
|
|
|
|
|
* Listmonk handles double opt-in (sends its own confirmation email)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function petitionSubscribeToNewsletter(string $email, string $name): bool {
|
|
|
|
|
|
$configPath = dirname(__DIR__, 2) . '/listmonk-config.php';
|
|
|
|
|
|
if (!file_exists($configPath)) {
|
|
|
|
|
|
error_log("Listmonk config not found: {$configPath}");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$config = require $configPath;
|
|
|
|
|
|
if (!$config['enabled']) {
|
|
|
|
|
|
error_log("Listmonk disabled in config");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Log the UUIDs being used
|
|
|
|
|
|
error_log("Listmonk attempting subscription with UUIDs: " . implode(', ', $config['list_uuids']));
|
|
|
|
|
|
|
|
|
|
|
|
$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("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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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);
|
2026-01-16 21:43:48 +01:00
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
// 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);
|
2026-01-16 21:43:48 +01:00
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
if (empty($petitionSlug)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-01-16 21:43:48 +01:00
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
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}";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 20:54:28 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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)
|
|
|
|
|
|
*/
|
2026-01-22 20:54:28 +01:00
|
|
|
|
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);
|
2026-01-16 00:37:13 +01:00
|
|
|
|
});
|
2026-01-22 20:54:28 +01:00
|
|
|
|
|
|
|
|
|
|
// Log the final result
|
|
|
|
|
|
petitionLogEmail('confirmation', $petitionId, $data['email'], $result, $result ? '' : ($errorMessage ?? 'Unknown error'));
|
|
|
|
|
|
|
|
|
|
|
|
return $result;
|
2026-01-16 00:37:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Internal: Send confirmation email (single attempt)
|
|
|
|
|
|
*/
|
2026-01-22 20:54:28 +01:00
|
|
|
|
function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl, string $petitionTitle, Context $ctx, ?string &$errorMessage = null): bool {
|
2026-01-16 00:37:13 +01:00
|
|
|
|
// 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)) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = 'SMTP config not found';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$config = require $smtpConfigPath;
|
|
|
|
|
|
|
|
|
|
|
|
if (!$config['enabled']) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = 'SMTP disabled';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
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');
|
|
|
|
|
|
|
2026-02-01 20:10:10 +01:00
|
|
|
|
// 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'];
|
|
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
// Pre-flight check
|
2026-02-01 20:10:10 +01:00
|
|
|
|
$fp = @fsockopen($smtpHost, $smtpPort, $errno, $errstr, 10);
|
2026-01-16 00:37:13 +01:00
|
|
|
|
if (!$fp) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = "Connection failed: {$errno} - {$errstr}";
|
2026-02-01 20:10:10 +01:00
|
|
|
|
error_log("Petition SMTP pre-flight failed: {$smtpHost}:{$smtpPort} - {$errno} - {$errstr}");
|
2026-01-16 00:37:13 +01:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
fclose($fp);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
require_once dirname(__DIR__, 2) . '/vendor/PHPMailer.Lite.php';
|
|
|
|
|
|
|
|
|
|
|
|
$mail = new \codeworxtech\PHPMailerLite\PHPMailerLite();
|
|
|
|
|
|
|
2026-02-01 20:10:10 +01:00
|
|
|
|
$mail->SetSMTPhost($smtpHost);
|
|
|
|
|
|
$mail->SetSMTPport($smtpPort);
|
|
|
|
|
|
$mail->SetSMTPuser($smtpUser);
|
|
|
|
|
|
$mail->SetSMTPpass($smtpPass);
|
2026-01-16 00:37:13 +01:00
|
|
|
|
|
|
|
|
|
|
$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) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = strip_tags($output) ?: 'Send returned false';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
error_log("Petition email send failed: " . strip_tags($output));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (\Exception $e) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = $e->getMessage();
|
2026-01-16 00:37:13 +01:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 00:14:34 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Send thank you email with delete link (with retry wrapper)
|
|
|
|
|
|
*/
|
2026-01-22 20:54:28 +01:00
|
|
|
|
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);
|
2026-01-16 00:37:13 +01:00
|
|
|
|
});
|
2026-01-22 20:54:28 +01:00
|
|
|
|
|
|
|
|
|
|
// Log the final result
|
|
|
|
|
|
petitionLogEmail('thankyou', $petitionId, $email, $result, $result ? '' : ($errorMessage ?? 'Unknown error'));
|
|
|
|
|
|
|
|
|
|
|
|
return $result;
|
2026-01-16 00:37:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Internal: Send thank you email with delete link (single attempt)
|
|
|
|
|
|
*/
|
2026-01-22 20:54:28 +01:00
|
|
|
|
function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, string $petitionTitle, string $csvPath, Context $ctx, ?string &$errorMessage = null): bool {
|
2026-01-16 00:37:13 +01:00
|
|
|
|
// 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) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = 'Signature not found';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php';
|
|
|
|
|
|
if (!file_exists($smtpConfigPath)) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = 'SMTP config not found';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$config = require $smtpConfigPath;
|
|
|
|
|
|
|
|
|
|
|
|
if (!$config['enabled']) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = 'SMTP disabled';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
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');
|
|
|
|
|
|
|
2026-02-01 20:10:10 +01:00
|
|
|
|
// 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'];
|
|
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
// Pre-flight check
|
2026-02-01 20:10:10 +01:00
|
|
|
|
$fp = @fsockopen($smtpHost, $smtpPort, $errno, $errstr, 10);
|
2026-01-16 00:37:13 +01:00
|
|
|
|
if (!$fp) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = "Connection failed: {$errno} - {$errstr}";
|
2026-02-01 20:10:10 +01:00
|
|
|
|
error_log("Petition SMTP pre-flight failed: {$smtpHost}:{$smtpPort} - {$errno} - {$errstr}");
|
2026-01-16 00:37:13 +01:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
fclose($fp);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
require_once dirname(__DIR__, 2) . '/vendor/PHPMailer.Lite.php';
|
|
|
|
|
|
|
|
|
|
|
|
$mail = new \codeworxtech\PHPMailerLite\PHPMailerLite();
|
|
|
|
|
|
|
2026-02-01 20:10:10 +01:00
|
|
|
|
$mail->SetSMTPhost($smtpHost);
|
|
|
|
|
|
$mail->SetSMTPport($smtpPort);
|
|
|
|
|
|
$mail->SetSMTPuser($smtpUser);
|
|
|
|
|
|
$mail->SetSMTPpass($smtpPass);
|
2026-01-16 00:37:13 +01:00
|
|
|
|
|
|
|
|
|
|
$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) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = strip_tags($output) ?: 'Send returned false';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
error_log("Petition thank you email send failed: " . strip_tags($output));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (\Exception $e) {
|
2026-01-22 20:54:28 +01:00
|
|
|
|
$errorMessage = $e->getMessage();
|
2026-01-16 00:37:13 +01:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-01-16 21:43:48 +01:00
|
|
|
|
$html = '<section class="petition-form">';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
// $html .= '<h2>' . htmlspecialchars(petitionT($ctx, 'petition', 'form_title')) . '</h2>';
|
|
|
|
|
|
|
|
|
|
|
|
// Error messages
|
|
|
|
|
|
if (!empty($errors)) {
|
|
|
|
|
|
$html .= '<div class="form-errors" role="alert">';
|
|
|
|
|
|
$html .= '<ul>';
|
|
|
|
|
|
foreach ($errors as $error) {
|
|
|
|
|
|
$html .= '<li>' . htmlspecialchars($error) . '</li>';
|
|
|
|
|
|
}
|
|
|
|
|
|
$html .= '</ul>';
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($showForm) {
|
|
|
|
|
|
$html .= '<form method="post" action="' . htmlspecialchars($_SERVER['REQUEST_URI']) . '" class="petition-form-inner">';
|
|
|
|
|
|
|
|
|
|
|
|
// Honeypot
|
|
|
|
|
|
$html .= '<div class="hp-field" aria-hidden="true" style="position:absolute;left:-9999px">';
|
|
|
|
|
|
$html .= '<label for="website">Website</label>';
|
|
|
|
|
|
$html .= '<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">';
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// Hidden fields
|
|
|
|
|
|
$html .= '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($csrfToken) . '">';
|
|
|
|
|
|
$html .= '<input type="hidden" name="form_start_time" value="' . $currentTime . '">';
|
|
|
|
|
|
|
|
|
|
|
|
// Name fields (firstname + surname on same row)
|
|
|
|
|
|
$html .= '<div class="form-row">';
|
|
|
|
|
|
|
|
|
|
|
|
$html .= '<div class="form-group">';
|
|
|
|
|
|
$html .= '<label for="petition_firstname">' . htmlspecialchars(petitionT($ctx, 'petition', 'firstname_label')) . ' <span class="required">*</span></label>';
|
|
|
|
|
|
$html .= '<input type="text" id="petition_firstname" name="firstname" value="' . htmlspecialchars($formData['firstname']) . '" required maxlength="50" placeholder="' . htmlspecialchars(petitionT($ctx, 'petition', 'firstname_placeholder')) . '">';
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
$html .= '<div class="form-group">';
|
|
|
|
|
|
$html .= '<label for="petition_surname">' . htmlspecialchars(petitionT($ctx, 'petition', 'surname_label')) . ' <span class="required">*</span></label>';
|
|
|
|
|
|
$html .= '<input type="text" id="petition_surname" name="surname" value="' . htmlspecialchars($formData['surname']) . '" required maxlength="50" placeholder="' . htmlspecialchars(petitionT($ctx, 'petition', 'surname_placeholder')) . '">';
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// Email field
|
|
|
|
|
|
$html .= '<div class="form-group">';
|
|
|
|
|
|
$html .= '<label for="petition_email">' . htmlspecialchars(petitionT($ctx, 'petition', 'email_label')) . ' <span class="required">*</span></label>';
|
|
|
|
|
|
$html .= '<input type="email" id="petition_email" name="email" value="' . htmlspecialchars($formData['email']) . '" required maxlength="100" placeholder="' . htmlspecialchars(petitionT($ctx, 'petition', 'email_placeholder')) . '">';
|
|
|
|
|
|
$html .= '<small>' . htmlspecialchars(petitionT($ctx, 'petition', 'email_help')) . '</small>';
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// Region dropdown
|
|
|
|
|
|
$html .= '<div class="form-group">';
|
|
|
|
|
|
$html .= '<label for="petition_region">' . htmlspecialchars(petitionT($ctx, 'petition', 'region_label')) . ' <span class="required">*</span></label>';
|
|
|
|
|
|
$html .= '<select id="petition_region" name="region" required>';
|
|
|
|
|
|
$html .= '<option value="">' . htmlspecialchars(petitionT($ctx, 'petition', 'region_placeholder')) . '</option>';
|
|
|
|
|
|
foreach ($regions as $key => $label) {
|
|
|
|
|
|
$selected = $formData['region'] === $key ? ' selected' : '';
|
|
|
|
|
|
$html .= '<option value="' . htmlspecialchars($key) . '"' . $selected . '>' . htmlspecialchars($label) . '</option>';
|
|
|
|
|
|
}
|
|
|
|
|
|
$html .= '</select>';
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// Display preference
|
|
|
|
|
|
$html .= '<fieldset class="form-group">';
|
|
|
|
|
|
$html .= '<legend>' . htmlspecialchars(petitionT($ctx, 'petition', 'display_legend')) . '</legend>';
|
|
|
|
|
|
|
|
|
|
|
|
$semiChecked = ($formData['display'] === 'semi' || empty($formData['display'])) ? ' checked' : '';
|
|
|
|
|
|
$html .= '<label class="radio-label">';
|
|
|
|
|
|
$html .= '<input type="radio" name="display" value="semi"' . $semiChecked . '>';
|
|
|
|
|
|
$html .= ' ' . htmlspecialchars(petitionT($ctx, 'petition', 'display_semi'));
|
|
|
|
|
|
$html .= '</label>';
|
|
|
|
|
|
|
|
|
|
|
|
$anonChecked = $formData['display'] === 'anonymous' ? ' checked' : '';
|
|
|
|
|
|
$html .= '<label class="radio-label">';
|
|
|
|
|
|
$html .= '<input type="radio" name="display" value="anonymous"' . $anonChecked . '>';
|
|
|
|
|
|
$html .= ' ' . htmlspecialchars(petitionT($ctx, 'petition', 'display_anonymous'));
|
|
|
|
|
|
$html .= '</label>';
|
|
|
|
|
|
|
|
|
|
|
|
$fullChecked = $formData['display'] === 'full' ? ' checked' : '';
|
|
|
|
|
|
$html .= '<label class="radio-label">';
|
|
|
|
|
|
$html .= '<input type="radio" name="display" value="full"' . $fullChecked . '>';
|
|
|
|
|
|
$html .= ' ' . htmlspecialchars(petitionT($ctx, 'petition', 'display_full'));
|
|
|
|
|
|
$html .= '</label>';
|
|
|
|
|
|
|
|
|
|
|
|
$html .= '</fieldset>';
|
|
|
|
|
|
|
|
|
|
|
|
// Privacy summary box
|
|
|
|
|
|
$langPrefix = $ctx->get('langPrefix', '');
|
|
|
|
|
|
$privacyUrl = $langPrefix . '/personvern/';
|
|
|
|
|
|
$html .= '<div class="form-group info-box info-box--yellow">';
|
|
|
|
|
|
$html .= '<p>' . petitionT($ctx, 'petition', 'privacy_summary') . ' ';
|
|
|
|
|
|
$html .= '<a href="' . htmlspecialchars($privacyUrl) . '" target="_blank">' . htmlspecialchars(petitionT($ctx, 'petition', 'privacy_policy_link')) . '</a></p>';
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
2026-01-16 21:43:48 +01:00
|
|
|
|
// GDPR Consent checkbox + Newsletter subscription
|
2026-01-16 00:37:13 +01:00
|
|
|
|
$consentChecked = isset($formData['gdpr_consent']) && $formData['gdpr_consent'] === 'on' ? ' checked' : '';
|
2026-01-16 21:43:48 +01:00
|
|
|
|
$newsletterChecked = isset($formData['newsletter']) && $formData['newsletter'] === 'on' ? ' checked' : '';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
$html .= '<div class="form-group info-box info-box--green consent-group">';
|
|
|
|
|
|
$html .= '<label class="consent-label">';
|
|
|
|
|
|
$html .= '<input type="checkbox" name="gdpr_consent" id="gdpr_consent" required' . $consentChecked . '> ';
|
|
|
|
|
|
$html .= '<span>' . petitionT($ctx, 'petition', 'gdpr_consent_text') . ' <span class="required">*</span></span>';
|
|
|
|
|
|
$html .= '</label>';
|
2026-01-16 21:43:48 +01:00
|
|
|
|
$html .= '<label class="consent-label">';
|
|
|
|
|
|
$html .= '<input type="checkbox" name="newsletter" id="newsletter"' . $newsletterChecked . '> ';
|
|
|
|
|
|
$html .= '<span>' . htmlspecialchars(petitionT($ctx, 'petition', 'newsletter_subscribe')) . '</span>';
|
|
|
|
|
|
$html .= '</label>';
|
2026-01-16 00:37:13 +01:00
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// Submit button
|
|
|
|
|
|
$html .= '<div class="form-group">';
|
|
|
|
|
|
$html .= '<button type="submit" name="petition_submit" class="button">' . htmlspecialchars(petitionT($ctx, 'petition', 'submit_button')) . '</button>';
|
|
|
|
|
|
$html .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
$html .= '</form>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$html .= '</section>';
|
|
|
|
|
|
|
|
|
|
|
|
return $html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Render the signatures list HTML
|
|
|
|
|
|
*/
|
|
|
|
|
|
function petitionRenderSignatures(array $signatures, Context $ctx): string {
|
|
|
|
|
|
$count = count($signatures);
|
|
|
|
|
|
|
|
|
|
|
|
$countText = $count === 1
|
|
|
|
|
|
? petitionT($ctx, 'petition', 'signature_count', ['count' => $count])
|
|
|
|
|
|
: petitionT($ctx, 'petition', 'signature_count_plural', ['count' => $count]);
|
|
|
|
|
|
|
|
|
|
|
|
$html = '<section class="petition-signatures">';
|
|
|
|
|
|
$html .= '<h2>' . htmlspecialchars(petitionT($ctx, 'petition', 'signatures_title')) . ' (' . $count . ')</h2>';
|
|
|
|
|
|
|
|
|
|
|
|
if ($count > 0) {
|
|
|
|
|
|
$html .= '<p class="signature-order">' . htmlspecialchars(petitionT($ctx, 'petition', 'newest_first')) . '</p>';
|
|
|
|
|
|
$html .= '<ul class="signature-list">';
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($signatures as $sig) {
|
|
|
|
|
|
$html .= '<li>';
|
|
|
|
|
|
$html .= '<span class="signature-name">' . petitionFormatSignature($sig, $ctx) . '</span>';
|
|
|
|
|
|
$html .= '<span class="signature-date">' . petitionFormatDate($sig['timestamp'], $ctx) . '</span>';
|
|
|
|
|
|
$html .= '</li>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$html .= '</ul>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$html .= '</section>';
|
|
|
|
|
|
|
|
|
|
|
|
return $html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Resolve URL slug path to actual filesystem path
|
|
|
|
|
|
*/
|
|
|
|
|
|
function petitionResolvePageDir(Context $ctx): ?string {
|
|
|
|
|
|
$requestPath = $ctx->requestPath;
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($requestPath)) {
|
|
|
|
|
|
return $ctx->contentDir;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$pathParts = explode('/', trim($requestPath, '/'));
|
|
|
|
|
|
$resolvedPath = $ctx->contentDir;
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($pathParts as $slug) {
|
|
|
|
|
|
$resolved = resolveSlugToFolder($resolvedPath, $slug);
|
|
|
|
|
|
if ($resolved === null) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
$resolvedPath .= '/' . $resolved;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $resolvedPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get petition data for the current page
|
|
|
|
|
|
* Can be called directly from PHP content files
|
|
|
|
|
|
*/
|
|
|
|
|
|
function petitionGetPageData(?Context $ctx): ?array {
|
|
|
|
|
|
static $cache = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Return cached result if already computed
|
|
|
|
|
|
if ($cache !== null) {
|
|
|
|
|
|
return $cache;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!$ctx) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Resolve the actual page directory from URL slugs
|
|
|
|
|
|
$pageDir = petitionResolvePageDir($ctx);
|
|
|
|
|
|
if (!$pageDir) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$metadata = loadMetadata($pageDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Only run if petition-form plugin is enabled
|
|
|
|
|
|
$plugins = $metadata['plugins'] ?? '';
|
|
|
|
|
|
if (strpos($plugins, 'petition-form') === false) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use petition_id from metadata if set, otherwise derive from URL path
|
|
|
|
|
|
$petitionId = $metadata['petition_id'] ?? petitionGetIdFromPath($pageDir);
|
|
|
|
|
|
if (!$petitionId) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-01-16 21:43:48 +01:00
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
$csvPath = petitionGetCsvPath($petitionId);
|
|
|
|
|
|
// loadMetadata() already merges language-specific metadata via Hook::PROCESS_CONTENT
|
2026-02-02 00:14:34 +01:00
|
|
|
|
// Use petition_title if set (for subpages), otherwise fall back to page title
|
|
|
|
|
|
$petitionTitle = $metadata['petition_title'] ?? $metadata['title'] ?? $petitionId;
|
2026-01-16 00:37:13 +01:00
|
|
|
|
$thankYouPage = $metadata['thank_you_page'] ?? 'takk';
|
|
|
|
|
|
|
|
|
|
|
|
$formErrors = [];
|
|
|
|
|
|
$formData = ['firstname' => '', 'surname' => '', 'email' => '', 'region' => '', 'display' => 'semi'];
|
|
|
|
|
|
$showForm = true;
|
|
|
|
|
|
$confirmMessage = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Check for errors and form data from session (Post/Redirect/Get pattern)
|
|
|
|
|
|
if (isset($_SESSION['petition_errors'])) {
|
|
|
|
|
|
$formErrors = $_SESSION['petition_errors'];
|
|
|
|
|
|
unset($_SESSION['petition_errors']);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isset($_SESSION['petition_form_data'])) {
|
|
|
|
|
|
$formData = array_merge($formData, $_SESSION['petition_form_data']);
|
|
|
|
|
|
unset($_SESSION['petition_form_data']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle confirmation (GET request with ?confirm=TOKEN)
|
|
|
|
|
|
if (isset($_GET['confirm']) && !empty($_GET['confirm'])) {
|
|
|
|
|
|
$token = preg_replace('/[^a-f0-9]/i', '', $_GET['confirm']);
|
|
|
|
|
|
$result = petitionConfirmSignature($csvPath, $token);
|
|
|
|
|
|
|
|
|
|
|
|
switch ($result) {
|
|
|
|
|
|
case 'success':
|
|
|
|
|
|
$confirmMessage = ['type' => 'success', 'text' => petitionT($ctx, 'petition', 'confirm_success')];
|
|
|
|
|
|
// Send thank you email with delete link
|
|
|
|
|
|
$langPrefix = $ctx->get('langPrefix', '');
|
|
|
|
|
|
$currentPath = trim($ctx->requestPath, '/');
|
|
|
|
|
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
|
|
|
|
|
$host = $_SERVER['HTTP_HOST'];
|
|
|
|
|
|
$deleteUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?delete={$token}#sign-now";
|
2026-01-22 20:54:28 +01:00
|
|
|
|
petitionSendThankYouEmail($token, $deleteUrl, $petitionTitle, $petitionId, $csvPath, $ctx);
|
2026-01-16 00:37:13 +01:00
|
|
|
|
break;
|
|
|
|
|
|
case 'already':
|
|
|
|
|
|
$confirmMessage = ['type' => 'info', 'text' => petitionT($ctx, 'petition', 'confirm_already')];
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'expired':
|
|
|
|
|
|
$confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'confirm_expired')];
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
$confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'confirm_error')];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle deletion (GET request with ?delete=TOKEN)
|
|
|
|
|
|
if (isset($_GET['delete']) && !empty($_GET['delete'])) {
|
|
|
|
|
|
$token = preg_replace('/[^a-f0-9]/i', '', $_GET['delete']);
|
|
|
|
|
|
$result = petitionDeleteSignature($csvPath, $token);
|
|
|
|
|
|
|
|
|
|
|
|
switch ($result) {
|
|
|
|
|
|
case 'success':
|
|
|
|
|
|
$confirmMessage = ['type' => 'warning', 'text' => petitionT($ctx, 'petition', 'delete_success')];
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
$confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'delete_error')];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 00:14:34 +01:00
|
|
|
|
// Handle resend confirmation request (POST from thank-you page)
|
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['petition_resend'])) {
|
|
|
|
|
|
$resendEmail = strtolower(trim($_POST['resend_email'] ?? ''));
|
|
|
|
|
|
|
|
|
|
|
|
// Rate limit check (reuse existing IP rate limiting)
|
|
|
|
|
|
if (!petitionCheckIPRateLimit($petitionId . '-resend', 3, 300)) {
|
|
|
|
|
|
$confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'resend_rate_limit')];
|
|
|
|
|
|
} elseif (empty($resendEmail) || !filter_var($resendEmail, FILTER_VALIDATE_EMAIL)) {
|
|
|
|
|
|
$confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'email_required')];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Look up signature by email
|
|
|
|
|
|
$signature = petitionGetPendingSignatureByEmail($csvPath, $resendEmail);
|
|
|
|
|
|
|
|
|
|
|
|
if ($signature === null) {
|
|
|
|
|
|
// Email not found at all
|
|
|
|
|
|
$confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'resend_not_found')];
|
|
|
|
|
|
} elseif ($signature['status'] === 'confirmed') {
|
|
|
|
|
|
// Already confirmed
|
|
|
|
|
|
$confirmMessage = ['type' => 'info', 'text' => petitionT($ctx, 'petition', 'resend_already_confirmed')];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Generate new token and update signature
|
|
|
|
|
|
$newToken = bin2hex(random_bytes(32));
|
|
|
|
|
|
if (petitionUpdateSignatureToken($csvPath, $resendEmail, $newToken)) {
|
|
|
|
|
|
// Build confirmation URL
|
|
|
|
|
|
$langPrefix = $ctx->get('langPrefix', '');
|
|
|
|
|
|
$currentPath = trim($ctx->requestPath, '/');
|
|
|
|
|
|
// Remove subpage suffixes to get petition base path
|
|
|
|
|
|
$currentPath = preg_replace('#/(takk|send-bekreftelse-pa-nytt)$#', '', $currentPath);
|
|
|
|
|
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
|
|
|
|
|
$host = $_SERVER['HTTP_HOST'];
|
|
|
|
|
|
$confirmUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?confirm={$newToken}#sign-now";
|
|
|
|
|
|
|
|
|
|
|
|
// Send confirmation email
|
|
|
|
|
|
$signatureData = [
|
|
|
|
|
|
'email' => $signature['email'],
|
|
|
|
|
|
'firstname' => $signature['firstname'],
|
|
|
|
|
|
'surname' => $signature['surname']
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (petitionSendConfirmationEmail($signatureData, $confirmUrl, $petitionTitle, $petitionId, $ctx)) {
|
|
|
|
|
|
$confirmMessage = ['type' => 'success', 'text' => petitionT($ctx, 'petition', 'resend_success')];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'error_email_send')];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'resend_not_found')];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Store message in session and redirect back (PRG pattern)
|
|
|
|
|
|
$_SESSION['petition_resend_message'] = $confirmMessage;
|
|
|
|
|
|
$langPrefix = $ctx->get('langPrefix', '');
|
|
|
|
|
|
$currentPath = trim($ctx->requestPath, '/');
|
|
|
|
|
|
header("Location: {$langPrefix}/{$currentPath}/");
|
|
|
|
|
|
exit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
// Handle form submission
|
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['petition_submit'])) {
|
|
|
|
|
|
|
|
|
|
|
|
// CSRF validation
|
|
|
|
|
|
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'error_csrf');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Honeypot check
|
|
|
|
|
|
if (!empty($_POST['website'])) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'error_honeypot');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Time-based check
|
|
|
|
|
|
$formStartTime = isset($_POST['form_start_time']) ? (int)$_POST['form_start_time'] : 0;
|
|
|
|
|
|
if (time() - $formStartTime < 3) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'error_time_check');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Referrer check
|
|
|
|
|
|
if (!empty($_SERVER['HTTP_REFERER'])) {
|
|
|
|
|
|
$referrer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
|
|
|
|
|
|
$currentHost = $_SERVER['HTTP_HOST'];
|
|
|
|
|
|
if ($referrer && $referrer !== $currentHost && $referrer !== 'localhost') {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'error_referrer');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GDPR Consent validation
|
|
|
|
|
|
if (!isset($_POST['gdpr_consent']) || $_POST['gdpr_consent'] !== 'on') {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'gdpr_consent_required');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Session-based rate limiting (prevents rapid resubmits)
|
|
|
|
|
|
$lastSubmitTime = $_SESSION['last_petition_submit'] ?? 0;
|
|
|
|
|
|
if (time() - $lastSubmitTime < 60) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'error_rate_limit');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IP-based rate limiting (prevents email bombing and spam)
|
|
|
|
|
|
// Max 3 attempts per 5 minutes per IP
|
|
|
|
|
|
if (empty($formErrors) && !petitionCheckIPRateLimit($petitionId, 3, 300)) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'error_ip_rate_limit');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get form data with normalization
|
|
|
|
|
|
$formData['firstname'] = trim($_POST['firstname'] ?? '');
|
|
|
|
|
|
$formData['surname'] = trim($_POST['surname'] ?? '');
|
|
|
|
|
|
$formData['email'] = strtolower(trim($_POST['email'] ?? '')); // Normalize email to lowercase
|
|
|
|
|
|
$formData['region'] = trim($_POST['region'] ?? '');
|
|
|
|
|
|
$formData['display'] = trim($_POST['display'] ?? 'semi');
|
|
|
|
|
|
$formData['gdpr_consent'] = $_POST['gdpr_consent'] ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
// Unicode normalization for names (if available)
|
|
|
|
|
|
if (function_exists('normalizer_normalize')) {
|
|
|
|
|
|
$formData['firstname'] = normalizer_normalize($formData['firstname'], Normalizer::FORM_C);
|
|
|
|
|
|
$formData['surname'] = normalizer_normalize($formData['surname'], Normalizer::FORM_C);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validation
|
|
|
|
|
|
if (empty($formData['firstname']) || strlen($formData['firstname']) < 2) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'firstname_required');
|
|
|
|
|
|
} elseif (strlen($formData['firstname']) > 50) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'firstname_required');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($formData['surname']) || strlen($formData['surname']) < 2) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'surname_required');
|
|
|
|
|
|
} elseif (strlen($formData['surname']) > 50) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'surname_required');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($formData['email']) || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'email_required');
|
|
|
|
|
|
} elseif (strlen($formData['email']) > 100) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'email_required');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate region against allowed list
|
|
|
|
|
|
$validRegions = array_keys(petitionGetRegions($ctx));
|
|
|
|
|
|
if (empty($formData['region']) || !in_array($formData['region'], $validRegions)) {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'region_required');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate display preference
|
|
|
|
|
|
if (!in_array($formData['display'], ['anonymous', 'semi', 'full'])) {
|
|
|
|
|
|
$formData['display'] = 'semi';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Note: Duplicate email check now happens inside petitionAppendSignature()
|
|
|
|
|
|
// to prevent race conditions with file locking
|
|
|
|
|
|
|
|
|
|
|
|
// If valid, save and send confirmation email
|
|
|
|
|
|
if (empty($formErrors)) {
|
|
|
|
|
|
$token = bin2hex(random_bytes(32));
|
|
|
|
|
|
$ipHash = hash('sha256', $_SERVER['REMOTE_ADDR'] . $petitionId);
|
|
|
|
|
|
$currentTime = time();
|
|
|
|
|
|
|
|
|
|
|
|
$signatureData = [
|
|
|
|
|
|
'timestamp' => $currentTime,
|
|
|
|
|
|
'email' => $formData['email'],
|
|
|
|
|
|
'firstname' => $formData['firstname'],
|
|
|
|
|
|
'surname' => $formData['surname'],
|
|
|
|
|
|
'region' => $formData['region'],
|
|
|
|
|
|
'display' => $formData['display'],
|
|
|
|
|
|
'status' => 'pending',
|
|
|
|
|
|
'token' => $token,
|
|
|
|
|
|
'token_created' => $currentTime,
|
|
|
|
|
|
'ip_hash' => $ipHash
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
$appendResult = petitionAppendSignature($csvPath, $signatureData);
|
|
|
|
|
|
|
|
|
|
|
|
if ($appendResult) {
|
|
|
|
|
|
// Build confirmation URL
|
|
|
|
|
|
$langPrefix = $ctx->get('langPrefix', '');
|
|
|
|
|
|
$currentPath = trim($ctx->requestPath, '/');
|
|
|
|
|
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
|
|
|
|
|
$host = $_SERVER['HTTP_HOST'];
|
|
|
|
|
|
$confirmUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?confirm={$token}#sign-now";
|
|
|
|
|
|
|
|
|
|
|
|
// Send confirmation email
|
2026-01-22 20:54:28 +01:00
|
|
|
|
if (petitionSendConfirmationEmail($signatureData, $confirmUrl, $petitionTitle, $petitionId, $ctx)) {
|
2026-01-16 21:43:48 +01:00
|
|
|
|
// Subscribe to newsletter if opted in (fire and forget - don't block petition)
|
|
|
|
|
|
$newsletterOptIn = isset($_POST['newsletter']) && $_POST['newsletter'] === 'on';
|
|
|
|
|
|
error_log("Newsletter checkbox: " . ($newsletterOptIn ? 'checked' : 'not checked'));
|
|
|
|
|
|
if ($newsletterOptIn) {
|
|
|
|
|
|
$fullName = $formData['firstname'] . ' ' . $formData['surname'];
|
|
|
|
|
|
petitionSubscribeToNewsletter($formData['email'], $fullName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
$_SESSION['last_petition_submit'] = time();
|
2026-01-16 21:43:48 +01:00
|
|
|
|
$_SESSION['petition_subscribed_newsletter'] = $newsletterOptIn;
|
2026-01-16 00:37:13 +01:00
|
|
|
|
|
|
|
|
|
|
// Regenerate session ID to prevent session fixation
|
|
|
|
|
|
session_regenerate_id(true);
|
|
|
|
|
|
|
|
|
|
|
|
// Redirect to thank you page
|
|
|
|
|
|
$thankYouUrl = "{$langPrefix}/{$currentPath}/{$thankYouPage}/";
|
|
|
|
|
|
header("Location: {$thankYouUrl}");
|
|
|
|
|
|
exit;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'error_email_send');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Append failed - likely due to duplicate email (checked inside lock)
|
|
|
|
|
|
$formErrors[] = petitionT($ctx, 'petition', 'error_already_signed');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If there are errors, store in session and redirect to show them
|
|
|
|
|
|
if (!empty($formErrors)) {
|
|
|
|
|
|
$_SESSION['petition_errors'] = $formErrors;
|
|
|
|
|
|
$_SESSION['petition_form_data'] = $formData;
|
2026-01-16 21:43:48 +01:00
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
$langPrefix = $ctx->get('langPrefix', '');
|
|
|
|
|
|
$currentPath = trim($ctx->requestPath, '/');
|
|
|
|
|
|
$redirectUrl = "{$langPrefix}/{$currentPath}/#sign-now";
|
|
|
|
|
|
header("Location: {$redirectUrl}");
|
|
|
|
|
|
exit;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get confirmed signatures
|
|
|
|
|
|
$signatures = petitionGetConfirmedSignatures($csvPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Build HTML
|
|
|
|
|
|
$formHtml = '';
|
|
|
|
|
|
|
|
|
|
|
|
// Show confirmation message if present
|
|
|
|
|
|
if ($confirmMessage) {
|
|
|
|
|
|
$formHtml .= '<div class="form-message form-message--' . $confirmMessage['type'] . '" role="alert">';
|
|
|
|
|
|
$formHtml .= '<p>' . htmlspecialchars($confirmMessage['text']) . '</p>';
|
|
|
|
|
|
$formHtml .= '</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$formHtml .= petitionRenderForm($ctx, $formData, $formErrors, $showForm);
|
|
|
|
|
|
$signaturesHtml = petitionRenderSignatures($signatures, $ctx);
|
|
|
|
|
|
|
|
|
|
|
|
// Cache and return results
|
|
|
|
|
|
$cache = [
|
|
|
|
|
|
'count' => count($signatures),
|
|
|
|
|
|
'form' => $formHtml,
|
|
|
|
|
|
'signatures' => $signaturesHtml
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return $cache;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Main Hook (for TEMPLATE_VARS) ---
|
|
|
|
|
|
|
|
|
|
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
|
|
|
|
|
$data = petitionGetPageData($ctx);
|
|
|
|
|
|
|
|
|
|
|
|
if ($data) {
|
|
|
|
|
|
$vars['petition_count'] = $data['count'];
|
|
|
|
|
|
$vars['petition_form'] = $data['form'];
|
|
|
|
|
|
$vars['petition_signatures'] = $data['signatures'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 21:43:48 +01:00
|
|
|
|
// Check for newsletter subscription flag (for thank you page)
|
|
|
|
|
|
if (isset($_SESSION['petition_subscribed_newsletter'])) {
|
|
|
|
|
|
$vars['petition_subscribed_newsletter'] = $_SESSION['petition_subscribed_newsletter'];
|
|
|
|
|
|
unset($_SESSION['petition_subscribed_newsletter']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 00:14:34 +01:00
|
|
|
|
// Check for resend confirmation message (for thank you page)
|
|
|
|
|
|
if (isset($_SESSION['petition_resend_message'])) {
|
|
|
|
|
|
$vars['petition_resend_message'] = $_SESSION['petition_resend_message'];
|
|
|
|
|
|
unset($_SESSION['petition_resend_message']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 00:37:13 +01:00
|
|
|
|
return $vars;
|
|
|
|
|
|
});
|