✨ 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
RecaptchaService.init();
|
||||||
window.contactFormValidator = new FormValidator('contact-form');
|
window.contactFormValidator = new FormValidator('contact-form');
|
||||||
window.contactFormPersistence = new ContactFormPersistence('contact-form');
|
window.contactFormPersistence = new ContactFormPersistence('contact-form');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Ready for Dev
|
review
|
||||||
|
|
||||||
## Story
|
## Story
|
||||||
|
|
||||||
@@ -21,29 +21,29 @@ Ready for Dev
|
|||||||
|
|
||||||
## Tasks / Subtasks
|
## Tasks / Subtasks
|
||||||
|
|
||||||
- [] **Task 1 : Configurer les clés reCAPTCHA** (AC: 5)
|
- [x] **Task 1 : Configurer les clés reCAPTCHA** (AC: 5)
|
||||||
- [] Ajouter RECAPTCHA_SITE_KEY dans .env
|
- [x] Ajouter RECAPTCHA_SITE_KEY dans .env
|
||||||
- [] Ajouter RECAPTCHA_SECRET_KEY dans .env
|
- [x] Ajouter RECAPTCHA_SECRET_KEY dans .env
|
||||||
- [] Créer includes/config.php pour charger .env et définir les constantes
|
- [x] Créer includes/config.php pour charger .env et définir les constantes
|
||||||
|
|
||||||
- [] **Task 2 : Charger le script Google** (AC: 2)
|
- [x] **Task 2 : Charger le script Google** (AC: 2)
|
||||||
- [] Ajouter le script dans templates/footer.php
|
- [x] Ajouter le script dans templates/footer.php
|
||||||
- [] Charger de manière asynchrone (async defer)
|
- [x] Charger de manière asynchrone (async defer)
|
||||||
- [] Exposer la site key via window.RECAPTCHA_SITE_KEY
|
- [x] Exposer la site key via window.RECAPTCHA_SITE_KEY
|
||||||
|
|
||||||
- [] **Task 3 : Générer le token** (AC: 3)
|
- [x] **Task 3 : Générer le token** (AC: 3)
|
||||||
- [] Créer RecaptchaService dans contact-form.js
|
- [x] Créer RecaptchaService dans contact-form.js
|
||||||
- [] Méthode getToken() avec grecaptcha.execute()
|
- [x] Méthode getToken() avec grecaptcha.execute()
|
||||||
- [] Retourne une Promise avec le token
|
- [x] Retourne une Promise avec le token
|
||||||
|
|
||||||
- [] **Task 4 : Envoyer le token au backend** (AC: 4)
|
- [x] **Task 4 : Envoyer le token au backend** (AC: 4)
|
||||||
- [] RecaptchaService.getToken() prêt à être utilisé
|
- [x] RecaptchaService.getToken() prêt à être utilisé
|
||||||
- [] Intégration avec AJAX dans Story 5.5/5.6
|
- [x] Intégration avec AJAX dans Story 5.5/5.6
|
||||||
|
|
||||||
- [] **Task 5 : Dégradation gracieuse** (AC: 6)
|
- [x] **Task 5 : Dégradation gracieuse** (AC: 6)
|
||||||
- [] isAvailable() vérifie si grecaptcha est défini
|
- [x] isAvailable() vérifie si grecaptcha est défini
|
||||||
- [] Retourne chaîne vide si indisponible
|
- [x] Retourne chaîne vide si indisponible
|
||||||
- [] console.warn si non disponible
|
- [x] console.warn si non disponible
|
||||||
|
|
||||||
## Dev Notes
|
## Dev Notes
|
||||||
|
|
||||||
@@ -223,7 +223,11 @@ Si reCAPTCHA échoue :
|
|||||||
## Dev Agent Record
|
## Dev Agent Record
|
||||||
|
|
||||||
### Agent Model Used
|
### 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 List
|
||||||
| File | Action | Description |
|
| File | Action | Description |
|
||||||
@@ -233,15 +237,14 @@ Claude Opus 4.5 (claude-opus-4-5-20251101)
|
|||||||
| `index.php` | Modified | Ajout require config.php |
|
| `index.php` | Modified | Ajout require config.php |
|
||||||
| `templates/footer.php` | Modified | Script reCAPTCHA + window.RECAPTCHA_SITE_KEY |
|
| `templates/footer.php` | Modified | Script reCAPTCHA + window.RECAPTCHA_SITE_KEY |
|
||||||
| `assets/js/contact-form.js` | Modified | Ajout RecaptchaService |
|
| `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
|
### Completion Notes
|
||||||
- Système de chargement .env avec loadEnv() dans config.php
|
- Chargement .env + constantes RECAPTCHA_* via config.php
|
||||||
- Constantes PHP : RECAPTCHA_SITE_KEY, RECAPTCHA_SECRET_KEY, APP_ENV, etc.
|
- Script Google async/defer + window.RECAPTCHA_SITE_KEY
|
||||||
- Script Google chargé en async/defer dans footer.php
|
- RecaptchaService init/isAvailable/getToken + dégradation gracieuse
|
||||||
- RecaptchaService avec méthodes init(), isAvailable(), getToken()
|
- Tests : `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
|
||||||
- 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
|
### Debug Log References
|
||||||
Aucun problème rencontré.
|
Aucun problème rencontré.
|
||||||
@@ -252,3 +255,4 @@ Aucun problème rencontré.
|
|||||||
|------|---------|-------------|--------|
|
|------|---------|-------------|--------|
|
||||||
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
||||||
| 2026-01-24 | 1.0 | Implémentation complète | James (Dev) |
|
| 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
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
require_once __DIR__ . '/includes/functions.php';
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
require_once __DIR__ . '/includes/router.php';
|
require_once __DIR__ . '/includes/router.php';
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,11 @@ $currentYear = date('Y');
|
|||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/assets/js/main.js" defer></script>
|
<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>
|
</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.test.php')
|
||||||
php (Join-Path $here 'contact-validation.test.php')
|
php (Join-Path $here 'contact-validation.test.php')
|
||||||
php (Join-Path $here 'contact-state.test.php')
|
php (Join-Path $here 'contact-state.test.php')
|
||||||
|
php (Join-Path $here 'recaptcha.test.php')
|
||||||
'OK'
|
'OK'
|
||||||
|
|||||||
Reference in New Issue
Block a user