# Story 5.4: Intégration reCAPTCHA v3 ## Status Ready for Dev ## Story **As a** propriétaire du site, **I want** une protection anti-spam invisible, **so that** je ne reçois pas de spam sans pénaliser l'expérience utilisateur. ## Acceptance Criteria 1. reCAPTCHA v3 est intégré (invisible, pas de case à cocher) 2. Le script reCAPTCHA est chargé depuis Google 3. Un token est généré à la soumission du formulaire 4. Le token est envoyé avec les données du formulaire au backend PHP 5. Les clés API (site key) sont configurables (fichier de config ou .env) 6. Si reCAPTCHA échoue à charger, le formulaire reste utilisable (dégradation gracieuse) ## Tasks / Subtasks - [] **Task 1 : Configurer les clés reCAPTCHA** (AC: 5) - [] Ajouter RECAPTCHA_SITE_KEY dans .env - [] Ajouter RECAPTCHA_SECRET_KEY dans .env - [] Créer includes/config.php pour charger .env et définir les constantes - [] **Task 2 : Charger le script Google** (AC: 2) - [] Ajouter le script dans templates/footer.php - [] Charger de manière asynchrone (async defer) - [] Exposer la site key via window.RECAPTCHA_SITE_KEY - [] **Task 3 : Générer le token** (AC: 3) - [] Créer RecaptchaService dans contact-form.js - [] Méthode getToken() avec grecaptcha.execute() - [] Retourne une Promise avec le token - [] **Task 4 : Envoyer le token au backend** (AC: 4) - [] RecaptchaService.getToken() prêt à être utilisé - [] Intégration avec AJAX dans Story 5.5/5.6 - [] **Task 5 : Dégradation gracieuse** (AC: 6) - [] isAvailable() vérifie si grecaptcha est défini - [] Retourne chaîne vide si indisponible - [] console.warn si non disponible ## Dev Notes ### Configuration .env ```env # reCAPTCHA v3 RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe ``` Note : Les clés ci-dessus sont les clés de test de Google (fonctionnent partout mais retournent toujours un score de 0.9). ### Exposer la Site Key (config.php ou header.php) ```php ``` ### Service JavaScript ```javascript // assets/js/contact-form.js (ajouter) /** * Service reCAPTCHA v3 */ const RecaptchaService = { siteKey: null, init() { this.siteKey = window.RECAPTCHA_SITE_KEY || null; }, isAvailable() { return this.siteKey && typeof grecaptcha !== 'undefined'; }, /** * Obtient un token reCAPTCHA * @param {string} action - Action à valider (ex: 'contact') * @returns {Promise} - Token ou chaîne vide si indisponible */ async getToken(action = 'contact') { // Dégradation gracieuse si reCAPTCHA non disponible if (!this.isAvailable()) { console.warn('reCAPTCHA non disponible, envoi sans protection'); return ''; } return new Promise((resolve, reject) => { grecaptcha.ready(() => { grecaptcha.execute(this.siteKey, { action }) .then(token => resolve(token)) .catch(error => { console.error('Erreur reCAPTCHA:', error); resolve(''); // Permettre l'envoi quand même }); }); }); } }; // Initialiser au chargement document.addEventListener('DOMContentLoaded', () => { RecaptchaService.init(); }); ``` ### Intégration dans l'Envoi du Formulaire ```javascript // Dans contact-form.js async function submitForm(formData) { // Obtenir le token reCAPTCHA const recaptchaToken = await RecaptchaService.getToken('contact'); // Ajouter aux données const payload = { ...formData, recaptcha_token: recaptchaToken }; // Envoyer au backend const response = await fetch('/api/contact.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload) }); return response.json(); } ``` ### Vérification Côté Serveur (api/contact.php) ```php /** * Vérifie le token reCAPTCHA v3 auprès de Google * @param string $token Token reçu du client * @return float Score (0.0 à 1.0), 0.0 si échec */ function verifyRecaptcha(string $token): float { // Si pas de token, retourner un score bas mais pas bloquant if (empty($token)) { error_log('reCAPTCHA: token vide'); return 0.3; // Score bas mais pas 0 } $response = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/x-www-form-urlencoded', 'content' => http_build_query([ 'secret' => RECAPTCHA_SECRET_KEY, 'response' => $token, 'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '' ]) ] ])); if ($response === false) { error_log('reCAPTCHA: impossible de contacter Google'); return 0.3; // Dégradation gracieuse } $result = json_decode($response, true); if (!($result['success'] ?? false)) { error_log('reCAPTCHA: échec - ' . json_encode($result['error-codes'] ?? [])); return 0.0; } return (float) ($result['score'] ?? 0.0); } ``` ### Seuil de Score | Score | Interprétation | Action | |-------|----------------|--------| | 0.9 - 1.0 | Très probablement humain | Accepter | | 0.5 - 0.9 | Probablement humain | Accepter | | 0.3 - 0.5 | Douteux | Accepter avec vigilance | | 0.0 - 0.3 | Probablement bot | Rejeter | Dans notre implémentation : seuil à 0.5 ### Dégradation Gracieuse Si reCAPTCHA échoue : 1. Le formulaire reste fonctionnel 2. Un avertissement est loggé 3. Le backend peut décider d'accepter ou non 4. Pas de message d'erreur visible pour l'utilisateur ## Testing - [] Le script reCAPTCHA se charge (vérifier Network) - [] Un token est généré à la soumission (RecaptchaService.getToken) - [] Le token est prêt à être envoyé au backend - [ ] Le backend vérifie le token avec Google (Story 5.5) - [] Si reCAPTCHA indisponible, le formulaire fonctionne quand même - [] Pas d'erreur visible si reCAPTCHA échoue (console.warn seulement) ## Dev Agent Record ### Agent Model Used Claude Opus 4.5 (claude-opus-4-5-20251101) ### File List | File | Action | Description | |------|--------|-------------| | `includes/config.php` | Created | Chargement .env et définition des constantes | | `.env` | Created | Variables d'environnement (clés test Google) | | `index.php` | Modified | Ajout require config.php | | `templates/footer.php` | Modified | Script reCAPTCHA + window.RECAPTCHA_SITE_KEY | | `assets/js/contact-form.js` | Modified | Ajout RecaptchaService | ### Completion Notes - Système de chargement .env avec loadEnv() dans config.php - Constantes PHP : RECAPTCHA_SITE_KEY, RECAPTCHA_SECRET_KEY, APP_ENV, etc. - Script Google chargé en async/defer dans footer.php - RecaptchaService avec méthodes init(), isAvailable(), getToken() - Dégradation gracieuse : retourne '' si reCAPTCHA indisponible - Clés de test Google utilisées en développement (score toujours 0.9) - La vérification côté serveur sera implémentée dans Story 5.5 ### Debug Log References Aucun problème rencontré. ## 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) |