innhold/docs/petition-system.md
Ruben a142b0562f Add documentation for content system, newsletter plugin, petition
system, and templates

Add content system documentation

Add newsletter plugin documentation

Add petition system documentation

Add templates documentation
2026-02-06 19:15:29 +01:00

6.3 KiB

Petition System Reference

For LLM agents working on the petition form. Read when modifying petition functionality.

Overview

GDPR-compliant petition system with double opt-in email confirmation. File-based storage (CSV) with proper locking. Located in custom/plugins/page/petition-form.php.

How It Activates

Add to a page's metadata.ini:

plugins = "petition-form"
petition_id = "my-petition"        # Optional, defaults to folder slug
thank_you_page = "takk"            # Subpage slug for post-submission redirect

The plugin is a page plugin (not global). It's loaded by the framework when the page is rendered, making its functions available to PHP content files.

User Flow

  1. User fills form (name, email, region, display preference, GDPR consent)
  2. Anti-spam checks: CSRF, honeypot, time-based, referrer, rate limiting (session + IP)
  3. Signature saved to CSV with status=pending and confirmation token
  4. Confirmation email sent via SMTP (with retry + exponential backoff)
  5. User redirected to thank-you page
  6. User clicks confirmation link -> ?confirm=TOKEN -> status changes to confirmed
  7. Thank-you email sent with delete link
  8. User can delete signature via ?delete=TOKEN (GDPR right to erasure)

CSV Format

File: custom/data/petitions/{petition_id}.csv

timestamp,email,firstname,surname,region,display,status,token,token_created,ip_hash
Column Description
timestamp Unix timestamp of submission
email Sanitized email (CSV injection protected)
firstname Sanitized first name
surname Sanitized last name
region Region key (e.g., oslo, vestland)
display Privacy preference: anonymous, semi, full
status pending or confirmed
token 64-char hex confirmation/delete token
token_created Unix timestamp for 30-day expiry
ip_hash SHA-256 of IP + petition_id

Key Functions

Function Purpose
petitionGetPageData(?Context) Main entry point. Returns all petition data for template rendering. Handles GET confirm/delete and POST submission.
petitionAppendSignature(csvPath, data) Atomic append with file locking + duplicate check inside lock
petitionConfirmSignature(csvPath, token) Confirms pending signature, checks 30-day token expiry
petitionDeleteSignature(csvPath, token) GDPR deletion - removes row entirely
petitionEmailExists(csvPath, email) Duplicate check (case-insensitive)
petitionGetConfirmedSignatures(csvPath) Returns confirmed signatures sorted newest-first
petitionRenderForm(ctx, formData, errors, showForm) Generates form HTML
petitionRenderSignatures(signatures, ctx) Generates signature list HTML
petitionSendConfirmationEmail(...) Sends confirmation with retry wrapper
petitionSendThankYouEmail(...) Sends thank-you with delete link
petitionCheckIPRateLimit(id, max, window) IP-based rate limiting (separate CSV file)
petitionT(ctx, section, key, replacements) Translation helper for petition strings
petitionGetPendingSignatureByEmail(csvPath, email) Lookup for resend functionality
petitionUpdateSignatureToken(csvPath, email, newToken) Token refresh for resend

Anti-Spam Measures

  1. CSRF token - Session-based, validated on submit
  2. Honeypot field - Hidden website field, rejects if filled
  3. Time check - Rejects if form submitted in <3 seconds
  4. Referrer check - Validates HTTP referrer matches host
  5. Session rate limit - 1 submission per 60 seconds
  6. IP rate limit - 3 attempts per 5 minutes per IP (stored in custom/data/petition-rate-limit.csv)
  7. CSV injection prevention - Prefixes dangerous characters with '

Email System

  • Uses PHPMailer.Lite (custom/vendor/PHPMailer.Lite.php)
  • Config in custom/smtp-config.php (not in repo, see smtp-config.php.example)
  • Supports petition-specific SMTP overrides in config
  • Retry with exponential backoff: 3 attempts at 2s, 4s, 8s delays
  • All sends logged to custom/data/smtp-log.csv
  • Pre-flight TCP connection check before attempting send

Resend Confirmation Flow

Subpage at send-bekreftelse-pa-nytt/ allows users to request a new confirmation email:

  1. User enters email address
  2. Plugin looks up pending signature by email
  3. Generates new token, updates CSV
  4. Sends new confirmation email
  5. Always shows generic message (privacy: doesn't reveal if email exists)

Content Structure

content/underskriftskampanje/medisinsk-cannabis-pa-resept/
  metadata.ini              # plugins="petition-form", petition_id, thank_you_page
  index.php                 # Calls petitionGetPageData(), renders form + signatures
  takk/
    metadata.ini            # plugins="petition-form" (needed for context)
    index.php               # Thank-you page with check-email message
  send-bekreftelse-pa-nytt/
    metadata.ini            # plugins="petition-form", petition_id, petition_title
    index.php               # Resend confirmation form

Translation Keys

All petition strings use petition.* prefix in language files. Key groups:

  • Form labels: petition.firstname_label, petition.email_label, etc.
  • Validation: petition.firstname_required, petition.email_required, etc.
  • Display options: petition.display_semi, petition.display_anonymous, petition.display_full
  • Email content: petition.email_greeting, petition.email_subject, etc.
  • Confirmation: petition.confirm_success, petition.confirm_expired, etc.
  • Regions: regions.oslo, regions.vestland, etc.

Critical: Do Not Break

  1. File locking - All CSV operations use flock(). Never bypass.
  2. Duplicate check inside lock - petitionAppendSignature() checks duplicates while holding exclusive lock to prevent race conditions.
  3. Token expiry - 30 days (2592000 seconds). Do not change without updating email text.
  4. PRG pattern - Form submission redirects via Post/Redirect/Get. Session stores errors/data.
  5. Privacy - Resend flow never reveals whether an email exists in the system.
  6. CSV sanitization - All user input goes through petitionSanitizeCSV() before writing.

CLI Tool

custom/petition-cli.php - Run inside container:

podman exec stopplidelsen.no php /var/www/custom/petition-cli.php [command] [args]

Commands: list signatures, confirm by email, delete by email, resend confirmation.