✨ Story 5.4: reCAPTCHA v3
This commit is contained in:
@@ -294,7 +294,38 @@ class ContactFormPersistence {
|
||||
}
|
||||
}
|
||||
|
||||
const RecaptchaService = {
|
||||
siteKey: null,
|
||||
|
||||
init() {
|
||||
this.siteKey = window.RECAPTCHA_SITE_KEY || null;
|
||||
},
|
||||
|
||||
isAvailable() {
|
||||
return this.siteKey && typeof grecaptcha !== 'undefined';
|
||||
},
|
||||
|
||||
async getToken(action = 'contact') {
|
||||
if (!this.isAvailable()) {
|
||||
console.warn('reCAPTCHA non disponible, envoi sans protection');
|
||||
return '';
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
grecaptcha.ready(() => {
|
||||
grecaptcha.execute(this.siteKey, { action })
|
||||
.then((token) => resolve(token))
|
||||
.catch((error) => {
|
||||
console.error('Erreur reCAPTCHA:', error);
|
||||
resolve('');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
RecaptchaService.init();
|
||||
window.contactFormValidator = new FormValidator('contact-form');
|
||||
window.contactFormPersistence = new ContactFormPersistence('contact-form');
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
review
|
||||
|
||||
## Story
|
||||
|
||||
@@ -21,29 +21,29 @@ Ready for Dev
|
||||
|
||||
## 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
|
||||
- [x] **Task 1 : Configurer les clés reCAPTCHA** (AC: 5)
|
||||
- [x] Ajouter RECAPTCHA_SITE_KEY dans .env
|
||||
- [x] Ajouter RECAPTCHA_SECRET_KEY dans .env
|
||||
- [x] 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
|
||||
- [x] **Task 2 : Charger le script Google** (AC: 2)
|
||||
- [x] Ajouter le script dans templates/footer.php
|
||||
- [x] Charger de manière asynchrone (async defer)
|
||||
- [x] 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
|
||||
- [x] **Task 3 : Générer le token** (AC: 3)
|
||||
- [x] Créer RecaptchaService dans contact-form.js
|
||||
- [x] Méthode getToken() avec grecaptcha.execute()
|
||||
- [x] 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
|
||||
- [x] **Task 4 : Envoyer le token au backend** (AC: 4)
|
||||
- [x] RecaptchaService.getToken() prêt à être utilisé
|
||||
- [x] 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
|
||||
- [x] **Task 5 : Dégradation gracieuse** (AC: 6)
|
||||
- [x] isAvailable() vérifie si grecaptcha est défini
|
||||
- [x] Retourne chaîne vide si indisponible
|
||||
- [x] console.warn si non disponible
|
||||
|
||||
## Dev Notes
|
||||
|
||||
@@ -223,7 +223,11 @@ Si reCAPTCHA échoue :
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
GPT-5 Codex
|
||||
|
||||
### Implementation Plan
|
||||
- Implémenter les tâches 1 à 5 dans l’ordre avec tests à chaque étape.
|
||||
- Ajouter config .env + chargement côté PHP, puis service JS reCAPTCHA.
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
@@ -233,15 +237,14 @@ Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
| `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 |
|
||||
| `tests/recaptcha.test.php` | Added | Tests recaptcha (config + scripts) |
|
||||
| `tests/run.ps1` | Modified | Ajout du test recaptcha |
|
||||
|
||||
### 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
|
||||
- Chargement .env + constantes RECAPTCHA_* via config.php
|
||||
- Script Google async/defer + window.RECAPTCHA_SITE_KEY
|
||||
- RecaptchaService init/isAvailable/getToken + dégradation gracieuse
|
||||
- Tests : `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
|
||||
|
||||
### Debug Log References
|
||||
Aucun problème rencontré.
|
||||
@@ -252,3 +255,4 @@ Aucun problème rencontré.
|
||||
|------|---------|-------------|--------|
|
||||
| 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 | Intégration reCAPTCHA v3 | Amelia (Dev) |
|
||||
|
||||
46
includes/config.php
Normal file
46
includes/config.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
function loadEnv(string $path): void
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || str_starts_with($line, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$key, $value] = array_pad(explode('=', $line, 2), 2, null);
|
||||
if ($key === null || $value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = trim($key);
|
||||
$value = trim($value);
|
||||
$value = trim($value, "\"'");
|
||||
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
putenv("{$key}={$value}");
|
||||
}
|
||||
}
|
||||
|
||||
function env(string $key, ?string $default = null): ?string
|
||||
{
|
||||
$value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
|
||||
if ($value === false || $value === null || $value === '') {
|
||||
return $default;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
loadEnv(__DIR__ . '/../.env');
|
||||
|
||||
define('RECAPTCHA_SITE_KEY', env('RECAPTCHA_SITE_KEY', ''));
|
||||
define('RECAPTCHA_SECRET_KEY', env('RECAPTCHA_SECRET_KEY', ''));
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/config.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
require_once __DIR__ . '/includes/router.php';
|
||||
|
||||
@@ -12,4 +13,4 @@ $router
|
||||
->add('/a-propos', 'pages/about.php')
|
||||
->add('/contact', 'pages/contact.php');
|
||||
|
||||
$router->dispatch();
|
||||
$router->dispatch();
|
||||
|
||||
@@ -15,5 +15,11 @@ $currentYear = date('Y');
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/assets/js/main.js" defer></script>
|
||||
<?php if (defined('RECAPTCHA_SITE_KEY') && RECAPTCHA_SITE_KEY): ?>
|
||||
<script>
|
||||
window.RECAPTCHA_SITE_KEY = '<?= htmlspecialchars(RECAPTCHA_SITE_KEY, ENT_QUOTES) ?>';
|
||||
</script>
|
||||
<script src="https://www.google.com/recaptcha/api.js?render=<?= htmlspecialchars(RECAPTCHA_SITE_KEY, ENT_QUOTES) ?>" async defer></script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
34
tests/recaptcha.test.php
Normal file
34
tests/recaptcha.test.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
function assertTrue($cond, $msg) {
|
||||
if (!$cond) {
|
||||
fwrite(STDERR, $msg . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$envPath = __DIR__ . '/../.env';
|
||||
assertTrue(file_exists($envPath), 'missing .env');
|
||||
$envContent = file_get_contents($envPath);
|
||||
assertTrue(strpos($envContent, 'RECAPTCHA_SITE_KEY') !== false, 'missing site key');
|
||||
assertTrue(strpos($envContent, 'RECAPTCHA_SECRET_KEY') !== false, 'missing secret key');
|
||||
|
||||
$configPath = __DIR__ . '/../includes/config.php';
|
||||
assertTrue(file_exists($configPath), 'missing config.php');
|
||||
$configContent = file_get_contents($configPath);
|
||||
assertTrue(strpos($configContent, 'RECAPTCHA_SITE_KEY') !== false, 'config missing site key');
|
||||
assertTrue(strpos($configContent, 'RECAPTCHA_SECRET_KEY') !== false, 'config missing secret key');
|
||||
|
||||
$indexContent = file_get_contents(__DIR__ . '/../index.php');
|
||||
assertTrue(strpos($indexContent, 'includes/config.php') !== false, 'index missing config require');
|
||||
|
||||
$footerContent = file_get_contents(__DIR__ . '/../templates/footer.php');
|
||||
assertTrue(strpos($footerContent, 'recaptcha/api.js') !== false, 'missing recaptcha script');
|
||||
assertTrue(strpos($footerContent, 'RECAPTCHA_SITE_KEY') !== false, 'missing site key exposure');
|
||||
|
||||
$contactJs = file_get_contents(__DIR__ . '/../assets/js/contact-form.js');
|
||||
assertTrue(strpos($contactJs, 'RecaptchaService') !== false, 'missing RecaptchaService');
|
||||
assertTrue(strpos($contactJs, 'getToken') !== false, 'missing getToken');
|
||||
assertTrue(strpos($contactJs, 'grecaptcha.execute') !== false, 'missing grecaptcha execute');
|
||||
assertTrue(strpos($contactJs, 'console.warn') !== false, 'missing graceful warning');
|
||||
|
||||
fwrite(STDOUT, "OK\n");
|
||||
@@ -23,4 +23,5 @@ php (Join-Path $here 'testimonials.test.php')
|
||||
php (Join-Path $here 'contact.test.php')
|
||||
php (Join-Path $here 'contact-validation.test.php')
|
||||
php (Join-Path $here 'contact-state.test.php')
|
||||
php (Join-Path $here 'recaptcha.test.php')
|
||||
'OK'
|
||||
|
||||
Reference in New Issue
Block a user