255 lines
7.7 KiB
Markdown
255 lines
7.7 KiB
Markdown
# 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
|
|
<!-- 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
|
|
|
|
```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
|
|
|
|
```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) |
|