Files
Portfolio-Codex/docs/stories/5.4.integration-recaptcha.md

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

  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

# 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 :

  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)