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
This commit is contained in:
parent
2f024e28be
commit
a142b0562f
4 changed files with 521 additions and 0 deletions
139
docs/petition-system.md
Normal file
139
docs/petition-system.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# 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`:
|
||||
```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:
|
||||
```bash
|
||||
podman exec stopplidelsen.no php /var/www/custom/petition-cli.php [command] [args]
|
||||
```
|
||||
|
||||
Commands: list signatures, confirm by email, delete by email, resend confirmation.
|
||||
Loading…
Add table
Add a link
Reference in a new issue