Ajout mode présentation

This commit is contained in:
Esteban committed 2026-08-28 00:04:48 +02:00
1 parent 4862d61ccd
commit 08875d3b5a
18 files changed
+1781 -270

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
<?php
// Fonctions liées au verrouillage anti-bruteforce du code PIN du "Mode présentation"
// (voir pages/presentation.php). Le compteur de tentatives est stocké côté serveur, par
// adresse IP (hashée, jamais en clair), pour ne pas pouvoir être remis à zéro en vidant
// juste les cookies ou en ouvrant une fenêtre de navigation privée.
require_once(__DIR__ . '/databaseConnection.php');
// Nombre de chiffres attendu pour le code PIN
define('PRESENTATION_PIN_LENGTH', 9);
// Durée du blocage une fois le nombre maximum de tentatives atteint (en heures)
define('PRESENTATION_LOCK_HOURS', 24);
// Hash SHA-256 de l'IP du visiteur : sert de clé dans la table presentation_attempts.
// On ne stocke jamais l'IP en clair, un hash suffit pour reconnaître le même visiteur
// d'une requête à l'autre sans conserver une donnée personnelle en clair.
function hashIpPresentation() {
return hash('sha256', $_SERVER['REMOTE_ADDR'] ?? 'inconnue');
}
// Vérifie le code PIN saisi contre le hash enregistré dans les réglages du site.
// Renvoie false si aucun code n'a encore été configuré (accès bloqué par défaut tant
// que l'admin n'a pas défini de code depuis le panel).
function verifierPinPresentation($pinSaisi) {
require_once(__DIR__ . '/listSettings.php');
$hash = recupererSetting('presentation_pin_hash');
if (empty($hash) || strlen($pinSaisi) !== PRESENTATION_PIN_LENGTH) {
return false;
}
return password_verify($pinSaisi, $hash);
}
// Récupère l'état de blocage actuel pour une IP (hashée) donnée.
// Renvoie ['attempts' => 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 '<p class="admin-notice">Aucune IP bloquée actuellement.</p>';
return;
}
echo '<div class="projects-list">';
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 '<div class="project-card">';
echo '<div class="project-content">';
echo '<h2>IP bloquée</h2>';
echo '<p><strong>Empreinte :</strong> ' . htmlspecialchars($empreinte) . '…</p>';
echo '<p><strong>Tentatives ratées :</strong> ' . (int) $ligne['attempts'] . '</p>';
echo '<p><strong>Bloquée jusqu\'au :</strong> ' . htmlspecialchars($jusqua) . '</p>';
echo '<div class="admin-actions">';
echo '<a href="../scripts/unlockPresentationAttempt.php?ip=' . urlencode($ligne['ip_hash'])
. '&csrf_token=' . urlencode(csrfToken()) . '" class="btn-submit btn-delete"'
. ' onclick="return confirm(\'Débloquer cette IP ?\');">Débloquer</a>';
echo '</div>';
echo '</div>';
echo '</div>';
}
echo '</div>';
}