Ajout mode présentation
This commit is contained in:
1 parent
4862d61ccd
commit
08875d3b5a
18 files changed
+1781
-270
No files matched your search
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
// Fonction qui construit l'URL d'un fichier statique (CSS, JS, image) en lui ajoutant un
|
||||
// numéro de version basé sur la date de dernière modification du fichier sur le disque.
|
||||
//
|
||||
// Pourquoi : le navigateur garde les fichiers CSS en cache parfois plusieurs jours, donc un
|
||||
// visiteur déjà venu continuait de voir l'ancien style après une mise en ligne (d'où la
|
||||
// notice "videz votre cache" affichée sur la page d'accueil). Avec "?v=1756329123" dans
|
||||
// l'URL, l'adresse change dès que le fichier change : le navigateur la considère comme une
|
||||
// nouvelle ressource et la retélécharge, sans qu'il ait à faire quoi que ce soit. Tant que
|
||||
// le fichier ne bouge pas, l'URL reste identique et le cache continue de servir (pas de
|
||||
// téléchargement inutile à chaque visite).
|
||||
//
|
||||
// filemtime plutôt que le hash du dernier commit : ça ne dépend pas de la présence de git
|
||||
// sur le serveur, et ça suit aussi les fichiers modifiés directement en production.
|
||||
//
|
||||
// $chemin : chemin du fichier depuis la racine du site (ex : 'styles/primary_css.css')
|
||||
// $prefixe : préfixe à mettre dans le HTML selon l'emplacement de la page qui l'appelle
|
||||
// ('../' pour les pages de /pages, './' pour les fichiers à la racine)
|
||||
function urlAsset($chemin, $prefixe = '../') {
|
||||
$fichier = __DIR__ . '/../' . $chemin;
|
||||
|
||||
// @ : si le fichier est introuvable on renvoie l'URL sans version plutôt que de
|
||||
// provoquer un warning PHP au milieu du <head>
|
||||
$version = @filemtime($fichier);
|
||||
|
||||
if ($version === false) {
|
||||
return $prefixe . $chemin;
|
||||
}
|
||||
|
||||
return $prefixe . $chemin . '?v=' . $version;
|
||||
}
|
||||
@@ -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>';
|
||||
}
|
||||
Reference in new issue
Block a user