Files
Portfolio-Codex/docs/stories/5.5.traitement-php-email.md
2026-02-04 21:22:13 +01:00

328 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Story 5.5: Traitement PHP et Envoi d'Email
## Status
review
## Story
**As a** propriétaire du site,
**I want** recevoir les messages par email de manière sécurisée,
**so that** je puisse répondre aux visiteurs.
## Acceptance Criteria
1. Le backend PHP valide à nouveau tous les champs (ne jamais faire confiance au client)
2. Le token reCAPTCHA est vérifié via l'API Google (score > 0.5)
3. Les données sont nettoyées (htmlspecialchars, trim) contre XSS
4. Un token CSRF est vérifié pour éviter les attaques cross-site
5. L'email est envoyé via `mail()` PHP ou SMTP configuré
6. L'email contient : tous les champs du formulaire, catégorie, date/heure, IP (optionnel)
7. En cas de succès, une réponse JSON `{"success": true}` est renvoyée
8. En cas d'erreur, une réponse JSON avec le message d'erreur est renvoyée
## Tasks / Subtasks
- [x] **Task 1 : Créer l'endpoint api/contact.php** (AC: 7, 8)
- [x] Créer le fichier api/contact.php
- [x] Configurer les headers JSON (Content-Type, X-Content-Type-Options)
- [x] Gérer uniquement les requêtes POST (405 sinon)
- [x] **Task 2 : Valider le token CSRF** (AC: 4)
- [x] Récupérer le token de la requête JSON
- [x] Utiliser verifyCsrfToken() existante
- [x] Exception si invalide
- [x] **Task 3 : Vérifier reCAPTCHA** (AC: 2)
- [x] Créer verifyRecaptcha() dans functions.php
- [x] Appeler l'API Google siteverify
- [x] Rejeter si score < RECAPTCHA_THRESHOLD (0.5)
- [x] **Task 4 : Valider les données** (AC: 1, 3)
- [x] Créer validateContactData() dans functions.php
- [x] Valider required, format email, longueurs min/max
- [x] Nettoyer avec htmlspecialchars et trim
- [x] Exception avec messages détaillés
- [x] **Task 5 : Envoyer l'email** (AC: 5, 6)
- [x] Créer sendContactEmail() dans functions.php
- [x] Corps formaté avec tous les champs + IP + date
- [x] Headers avec Reply-To vers l'expéditeur
- [x] **Task 6 : Retourner la réponse** (AC: 7, 8)
- [x] JSON {"success": true, "message": "..."} si OK
- [x] JSON {"success": false, "error": "..."} si erreur
## Dev Notes
### Endpoint api/contact.php
```php
<?php
/**
* Endpoint de traitement du formulaire de contact
*/
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../includes/functions.php';
// Headers
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
// Uniquement POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
exit;
}
// Récupérer les données JSON
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Données invalides']);
exit;
}
try {
// 1. Valider le token CSRF
if (!validateCsrfToken($input['csrf_token'] ?? '')) {
throw new Exception('Token de sécurité invalide. Veuillez rafraîchir la page.');
}
// 2. Vérifier reCAPTCHA
$recaptchaScore = verifyRecaptcha($input['recaptcha_token'] ?? '');
if ($recaptchaScore < RECAPTCHA_THRESHOLD) {
error_log("reCAPTCHA score trop bas: {$recaptchaScore}");
throw new Exception('Vérification anti-spam échouée. Veuillez réessayer.');
}
// 3. Valider et nettoyer les données
$data = validateContactData($input);
// 4. Envoyer l'email
$sent = sendContactEmail($data);
if (!$sent) {
throw new Exception('Erreur lors de l\'envoi du message. Veuillez réessayer plus tard.');
}
// 5. Succès
echo json_encode([
'success' => true,
'message' => 'Votre message a bien été envoyé ! Je vous répondrai dans les meilleurs délais.'
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
```
### Fonction de Validation (includes/functions.php)
```php
/**
* Valide et nettoie les données du formulaire de contact
* @throws Exception si validation échoue
*/
function validateContactData(array $input): array
{
$errors = [];
// Champs requis
$required = ['nom', 'prenom', 'email', 'categorie', 'objet', 'message'];
foreach ($required as $field) {
if (empty(trim($input[$field] ?? ''))) {
$errors[] = "Le champ {$field} est requis";
}
}
// Validation email
$email = trim($input['email'] ?? '');
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "L'adresse email n'est pas valide";
}
// Validation catégorie
$validCategories = ['projet', 'poste', 'autre'];
$categorie = $input['categorie'] ?? '';
if ($categorie && !in_array($categorie, $validCategories)) {
$errors[] = "Catégorie invalide";
}
// Validation longueurs
if (strlen($input['nom'] ?? '') > 100) {
$errors[] = "Le nom est trop long (max 100 caractères)";
}
if (strlen($input['prenom'] ?? '') > 100) {
$errors[] = "Le prénom est trop long (max 100 caractères)";
}
if (strlen($input['objet'] ?? '') > 200) {
$errors[] = "L'objet est trop long (max 200 caractères)";
}
if (strlen($input['message'] ?? '') > 5000) {
$errors[] = "Le message est trop long (max 5000 caractères)";
}
// Si erreurs, les lancer
if (!empty($errors)) {
throw new Exception(implode('. ', $errors));
}
// Nettoyer et retourner
return [
'nom' => htmlspecialchars(trim($input['nom']), ENT_QUOTES, 'UTF-8'),
'prenom' => htmlspecialchars(trim($input['prenom']), ENT_QUOTES, 'UTF-8'),
'email' => filter_var(trim($input['email']), FILTER_SANITIZE_EMAIL),
'entreprise' => htmlspecialchars(trim($input['entreprise'] ?? ''), ENT_QUOTES, 'UTF-8'),
'categorie' => $input['categorie'],
'objet' => htmlspecialchars(trim($input['objet']), ENT_QUOTES, 'UTF-8'),
'message' => htmlspecialchars(trim($input['message']), ENT_QUOTES, 'UTF-8'),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'inconnue',
'date' => date('d/m/Y à H:i:s'),
];
}
```
### Fonction d'Envoi d'Email
```php
/**
* Envoie l'email de contact
*/
function sendContactEmail(array $data): bool
{
$categorieLabels = [
'projet' => 'Projet freelance',
'poste' => 'Proposition de poste',
'autre' => 'Autre demande'
];
$subject = "[Portfolio] {$categorieLabels[$data['categorie']]} - {$data['objet']}";
$body = <<<EMAIL
═══════════════════════════════════════════
NOUVEAU MESSAGE - PORTFOLIO
═══════════════════════════════════════════
DE: {$data['prenom']} {$data['nom']}
EMAIL: {$data['email']}
ENTREPRISE: {$data['entreprise']}
CATÉGORIE: {$categorieLabels[$data['categorie']]}
───────────────────────────────────────────
OBJET: {$data['objet']}
───────────────────────────────────────────
MESSAGE:
{$data['message']}
═══════════════════════════════════════════
Envoyé le {$data['date']}
IP: {$data['ip']}
═══════════════════════════════════════════
EMAIL;
$headers = implode("\r\n", [
'From: ' . CONTACT_EMAIL,
'Reply-To: ' . $data['email'],
'Content-Type: text/plain; charset=UTF-8',
'X-Mailer: PHP/' . phpversion(),
'X-Priority: 1'
]);
$result = mail(CONTACT_EMAIL, $subject, $body, $headers);
if (!$result) {
error_log("Échec envoi email contact: " . print_r($data, true));
}
return $result;
}
```
### Sécurité Implémentée
| Menace | Protection |
|--------|------------|
| XSS | htmlspecialchars() sur toutes les entrées |
| CSRF | Token vérifié en session |
| Spam | reCAPTCHA v3 avec seuil 0.5 |
| Injection | filter_var() pour l'email |
| Email header injection | Pas de \r\n dans les champs utilisateur |
### Structure de la Réponse JSON
**Succès :**
```json
{
"success": true,
"message": "Votre message a bien été envoyé !"
}
```
**Erreur :**
```json
{
"success": false,
"error": "Message d'erreur explicite"
}
```
## Testing
- [] Les données sont validées côté serveur (validateContactData)
- [] Le token CSRF est vérifié (verifyCsrfToken)
- [] Le score reCAPTCHA est vérifié (verifyRecaptcha)
- [] Les données sont nettoyées (htmlspecialchars, filter_var)
- [] L'email est envoyé avec tous les champs (sendContactEmail)
- [] La réponse JSON est correcte (succès)
- [] La réponse JSON est correcte (erreur avec message)
- [] Les erreurs sont loggées (error_log)
## Dev Agent Record
### Agent Model Used
GPT-5 Codex
### Implementation Plan
- Implémenter les tâches 1 à 6 dans lordre avec tests à chaque étape.
- Ajouter endpoint API et fonctions de validation/envoi.
### File List
| File | Action | Description |
|------|--------|-------------|
| `api/contact.php` | Created | Endpoint de traitement du formulaire |
| `includes/functions.php` | Modified | Ajout verifyRecaptcha, validateContactData, sendContactEmail |
| `includes/config.php` | Modified | Ajout RECAPTCHA_THRESHOLD + CONTACT_EMAIL |
| `.env` | Modified | Ajout CONTACT_EMAIL |
| `tests/contact-api.test.php` | Added | Tests endpoint contact |
| `tests/run.ps1` | Modified | Ajout du test contact-api |
### Completion Notes
- Endpoint api/contact.php avec gestion JSON complète
- verifyRecaptcha() : appel API Google + seuil 0.5 + dégradation
- validateContactData() : validation/normalisation complète
- sendContactEmail() : email formaté avec Reply-To, IP, date
- Sécurité : CSRF, reCAPTCHA, htmlspecialchars, filter_var
- Tests : `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
### Debug Log References
- Correction syntaxe heredoc (EMAIL: interprété comme label)
## Change Log
| Date | Version | Description | Author |
|------|---------|-------------|--------|
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
| 2026-01-24 | 1.0 | Implémentation complète | James (Dev) |
| 2026-02-04 | 1.1 | Endpoint contact + validation serveur | Amelia (Dev) |