int, 'locked_until' => string|null] ou null si cette IP n'a // encore jamais raté de tentative. function recupererTentativePresentation($ipHash) { global $dbh; try { $stmt = $dbh->prepare('SELECT attempts, locked_until FROM presentation_attempts WHERE ip_hash = :ip'); $stmt->bindValue(':ip', $ipHash, PDO::PARAM_STR); $stmt->execute(); $ligne = $stmt->fetch(PDO::FETCH_ASSOC); return $ligne ?: null; } catch (PDOException $e) { // Tant que la table n'existe pas encore (migration SQL pas encore appliquée), // on considère qu'il n'y a pas de blocage plutôt que de casser la page return null; } } // Vrai si cette IP est actuellement bloquée (trop de tentatives ratées récemment) function presentationEstVerrouillee($ipHash, $maxTentatives) { $etat = recupererTentativePresentation($ipHash); return $etat && (int) $etat['attempts'] >= $maxTentatives && !empty($etat['locked_until']) && strtotime($etat['locked_until']) > time(); } // Enregistre une tentative ratée : incrémente le compteur de cette IP, et pose un // blocage de PRESENTATION_LOCK_HOURS heures si le maximum est atteint. // Renvoie le nouvel état, au même format que recupererTentativePresentation(). function enregistrerEchecPresentation($ipHash, $maxTentatives) { global $dbh; $etatActuel = recupererTentativePresentation($ipHash); $tentatives = $etatActuel ? (int) $etatActuel['attempts'] + 1 : 1; $verrouJusqua = $tentatives >= $maxTentatives ? date('Y-m-d H:i:s', time() + PRESENTATION_LOCK_HOURS * 3600) : null; try { $stmt = $dbh->prepare( 'INSERT INTO presentation_attempts (ip_hash, attempts, locked_until) VALUES (:ip, :tentatives, :verrou) ON DUPLICATE KEY UPDATE attempts = VALUES(attempts), locked_until = VALUES(locked_until)' ); $stmt->execute([ ':ip' => $ipHash, ':tentatives' => $tentatives, ':verrou' => $verrouJusqua ]); } catch (PDOException $e) { // Si la table n'existe pas encore, on ne bloque pas le visiteur pour autant } return ['attempts' => $tentatives, 'locked_until' => $verrouJusqua]; } // Efface le compteur de tentatives d'une IP : code correct, connexion admin, ou // déblocage manuel depuis le panel admin function reinitialiserTentativesPresentation($ipHash) { global $dbh; try { $stmt = $dbh->prepare('DELETE FROM presentation_attempts WHERE ip_hash = :ip'); $stmt->bindValue(':ip', $ipHash, PDO::PARAM_STR); $stmt->execute(); } catch (PDOException $e) { // Rien à faire si la table n'existe pas encore } } // Liste des IP actuellement bloquées, pour le panel admin function listerTentativesVerrouillees() { global $dbh; try { $stmt = $dbh->query( 'SELECT ip_hash, attempts, locked_until FROM presentation_attempts WHERE locked_until IS NOT NULL AND locked_until > NOW() ORDER BY locked_until DESC' ); return $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { return []; } } // Affiche, dans le panel admin, la liste des IP actuellement bloquées avec un bouton // de déblocage manuel pour chacune (utile si un visiteur légitime se fait bloquer) function afficherTentativesVerrouilleesAdmin() { $lignes = listerTentativesVerrouillees(); if (empty($lignes)) { echo '

Aucune IP bloquée actuellement.

'; return; } echo '
'; foreach ($lignes as $ligne) { // On n'affiche jamais l'IP en clair : seulement une courte empreinte de son hash, // suffisante pour distinguer deux blocages différents sans exposer de donnée perso $empreinte = substr($ligne['ip_hash'], 0, 12); $jusqua = date('d/m/Y à H:i', strtotime($ligne['locked_until'])); echo '
'; echo '
'; echo '

IP bloquée

'; echo '

Empreinte : ' . htmlspecialchars($empreinte) . '…

'; echo '

Tentatives ratées : ' . (int) $ligne['attempts'] . '

'; echo '

Bloquée jusqu\'au : ' . htmlspecialchars($jusqua) . '

'; echo '
'; echo 'Débloquer'; echo '
'; echo '
'; echo '
'; } echo '
'; }