Merge branch 'latest' of forge.dmz.skyfritt.net:stopplidelsen/innhold into latest

This commit is contained in:
Ruben Solvang 2026-02-02 00:15:02 +01:00
commit 702f2cd73e
7 changed files with 208 additions and 1 deletions

View file

@ -1,4 +1,7 @@
<h1 id="sign-now">Signer her for å vise din støtte</h1> <h1 id="sign-now">Signer her for å vise din støtte</h1>
<?= $petition_form ?? '' ?> <?= $petition_form ?? '' ?>
<p class="text-small text-muted">Har du allerede signert, men ikke mottatt bekreftelsesmail? <a href="send-bekreftelse-pa-nytt/">Send bekreftelse nytt</a></p>
<?= $petition_signatures ?? '' ?> <?= $petition_signatures ?? '' ?>

View file

@ -0,0 +1,21 @@
<h1>Send bekreftelse nytt</h1>
<p>Skriv inn e-postadressen du brukte da du signerte, sender vi en ny bekreftelseslenke.</p>
<?php if (!empty($petition_resend_message)): ?>
<div class="form-message form-message--<?= htmlspecialchars($petition_resend_message['type']) ?>" role="alert">
<p><?= htmlspecialchars($petition_resend_message['text']) ?></p>
</div>
<?php endif; ?>
<form method="post" action="" class="resend-form">
<div class="form-group">
<label for="resend_email">E-post</label>
<input type="email" id="resend_email" name="resend_email" placeholder="din@epost.no" required maxlength="100">
</div>
<div class="form-group">
<button type="submit" name="petition_resend" class="button">Send nytt</button>
</div>
</form>
<p><a href="../">Tilbake til underskriftskampanjen</a></p>

View file

@ -0,0 +1,8 @@
title = "Send bekreftelse på nytt"
plugins = "petition-form"
petition_id = "medisinsk-cannabis-pa-resept"
petition_title = "Underskriftskampanje: Ja til medisinsk cannabis på resept!"
[en]
title = "Resend confirmation"
petition_title = "Petition: Yes to medical cannabis on prescription!"

View file

@ -0,0 +1 @@
@import url("../styles.css");

View file

@ -8,4 +8,6 @@
<?php endif; ?> <?php endif; ?>
</div> </div>
<p>Fikk du ikke e-posten? <a href="../send-bekreftelse-pa-nytt/">Send bekreftelse nytt</a></p>
<p><a href="../">Tilbake til underskriftskampanjen</a></p> <p><a href="../">Tilbake til underskriftskampanjen</a></p>

View file

@ -79,6 +79,14 @@ gdpr_consent_text = "Jeg har lest <a href=\"/personvern/\" target=\"_blank\">per
gdpr_consent_required = "Du må samtykke til personvernerklæringen for å signere." gdpr_consent_required = "Du må samtykke til personvernerklæringen for å signere."
newsletter_subscribe = "Jeg ønsker å motta nyhetsbrev fra Stopp Lidelsen (omtrent 12 i året). Du kan melde deg av når som helst." newsletter_subscribe = "Jeg ønsker å motta nyhetsbrev fra Stopp Lidelsen (omtrent 12 i året). Du kan melde deg av når som helst."
email_rights_info = "Du har rett til innsyn, retting og sletting av dine opplysninger. Kontakt oss på kontakt@stopplidelsen.no eller klag til Datatilsynet (datatilsynet.no)." email_rights_info = "Du har rett til innsyn, retting og sletting av dine opplysninger. Kontakt oss på kontakt@stopplidelsen.no eller klag til Datatilsynet (datatilsynet.no)."
resend_title = "Fikk du ikke e-posten?"
resend_description = "Skriv inn e-postadressen du brukte da du signerte, så sender vi en ny bekreftelseslenke."
resend_email_placeholder = "din@epost.no"
resend_submit = "Send på nytt"
resend_success = "Hvis e-postadressen finnes i vårt system, har vi sendt en ny bekreftelseslenke."
resend_not_found = "Vi fant ingen ubekreftet signatur med denne e-postadressen. Kanskje du skrev feil da du signerte? Du kan gjerne prøve å signere på nytt."
resend_already_confirmed = "Denne signaturen er allerede bekreftet."
resend_rate_limit = "Vennligst vent litt før du ber om en ny e-post."
[regions] [regions]
agder = "Agder" agder = "Agder"

View file

@ -799,6 +799,105 @@ function petitionGetSignatureByToken(string $csvPath, string $token): ?array {
return $signature; 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) * Send thank you email with delete link (with retry wrapper)
*/ */
@ -1136,7 +1235,8 @@ function petitionGetPageData(?Context $ctx): ?array {
$csvPath = petitionGetCsvPath($petitionId); $csvPath = petitionGetCsvPath($petitionId);
// loadMetadata() already merges language-specific metadata via Hook::PROCESS_CONTENT // loadMetadata() already merges language-specific metadata via Hook::PROCESS_CONTENT
$petitionTitle = $metadata['title'] ?? $petitionId; // Use petition_title if set (for subpages), otherwise fall back to page title
$petitionTitle = $metadata['petition_title'] ?? $metadata['title'] ?? $petitionId;
$thankYouPage = $metadata['thank_you_page'] ?? 'takk'; $thankYouPage = $metadata['thank_you_page'] ?? 'takk';
$formErrors = []; $formErrors = [];
@ -1195,6 +1295,64 @@ function petitionGetPageData(?Context $ctx): ?array {
} }
} }
// 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;
}
// Handle form submission // Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['petition_submit'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['petition_submit'])) {
@ -1401,5 +1559,11 @@ Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
unset($_SESSION['petition_subscribed_newsletter']); unset($_SESSION['petition_subscribed_newsletter']);
} }
// 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']);
}
return $vars; return $vars;
}); });