7.7 KiB
7.7 KiB
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
- reCAPTCHA v3 est intégré (invisible, pas de case à cocher)
- Le script reCAPTCHA est chargé depuis Google
- Un token est généré à la soumission du formulaire
- Le token est envoyé avec les données du formulaire au backend PHP
- Les clés API (site key) sont configurables (fichier de config ou .env)
- 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
# 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)
<!-- Dans templates/footer.php ou avant </body> -->
<?php if (defined('RECAPTCHA_SITE_KEY') && RECAPTCHA_SITE_KEY): ?>
<script>
window.RECAPTCHA_SITE_KEY = '<?= RECAPTCHA_SITE_KEY ?>';
</script>
<script src="https://www.google.com/recaptcha/api.js?render=<?= RECAPTCHA_SITE_KEY ?>" async defer></script>
<?php endif; ?>
Service 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<string>} - 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
// 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)
/**
* 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 :
- Le formulaire reste fonctionnel
- Un avertissement est loggé
- Le backend peut décider d'accepter ou non
- 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) |