# Story 5.5: Traitement PHP et Envoi d'Email ## Status Ready for Dev ## 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 - [] **Task 1 : Créer l'endpoint api/contact.php** (AC: 7, 8) - [] Créer le fichier api/contact.php - [] Configurer les headers JSON (Content-Type, X-Content-Type-Options) - [] Gérer uniquement les requêtes POST (405 sinon) - [] **Task 2 : Valider le token CSRF** (AC: 4) - [] Récupérer le token de la requête JSON - [] Utiliser verifyCsrfToken() existante - [] Exception si invalide - [] **Task 3 : Vérifier reCAPTCHA** (AC: 2) - [] Créer verifyRecaptcha() dans functions.php - [] Appeler l'API Google siteverify - [] Rejeter si score < RECAPTCHA_THRESHOLD (0.5) - [] **Task 4 : Valider les données** (AC: 1, 3) - [] Créer validateContactData() dans functions.php - [] Valider required, format email, longueurs min/max - [] Nettoyer avec htmlspecialchars et trim - [] Exception avec messages détaillés - [] **Task 5 : Envoyer l'email** (AC: 5, 6) - [] Créer sendContactEmail() dans functions.php - [] Corps formaté avec tous les champs + IP + date - [] Headers avec Reply-To vers l'expéditeur - [] **Task 6 : Retourner la réponse** (AC: 7, 8) - [] JSON {"success": true, "message": "..."} si OK - [] JSON {"success": false, "error": "..."} si erreur ## Dev Notes ### Endpoint api/contact.php ```php 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 = <<