Patch captcha
This commit is contained in:
+137
-47
@@ -20,6 +20,11 @@ if (isset($_SESSION['captcha_valid']) && $_SESSION['captcha_valid'] === true) {
|
|||||||
// Nombre maximum d'essais
|
// Nombre maximum d'essais
|
||||||
$maxAttempts = 3;
|
$maxAttempts = 3;
|
||||||
|
|
||||||
|
// Durée minimale (en secondes) entre l'affichage de la série et la soumission du formulaire.
|
||||||
|
// Un humain doit lire plusieurs messages avant de répondre : une soumission plus rapide
|
||||||
|
// que ça trahit presque toujours un robot qui remplit le formulaire instantanément.
|
||||||
|
$minSolveTime = 1.2;
|
||||||
|
|
||||||
// Liste des sujets normaux
|
// Liste des sujets normaux
|
||||||
$normalSubjects = [
|
$normalSubjects = [
|
||||||
'Votre rendez-vous est confirmé',
|
'Votre rendez-vous est confirmé',
|
||||||
@@ -150,28 +155,29 @@ $normalSenders = [
|
|||||||
'Service facturation'
|
'Service facturation'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Liste des sujets spam
|
// Liste des sujets spam, chacun associé à un indice (raison) qui sera révélé après succès :
|
||||||
|
// une petite touche pédagogique sur les techniques de phishing les plus courantes
|
||||||
$spamSubjects = [
|
$spamSubjects = [
|
||||||
'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURD’HUI',
|
['text' => 'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURD’HUI', 'reason' => 'Urgence exagérée et abus des majuscules'],
|
||||||
'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT',
|
['text' => 'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT', 'reason' => 'Gain surprise, trop beau pour être vrai'],
|
||||||
'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE',
|
['text' => 'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE', 'reason' => 'Appel à l’action pressant et menaçant'],
|
||||||
'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE',
|
['text' => 'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE', 'reason' => 'Demande de paiement immédiat inhabituelle'],
|
||||||
'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE',
|
['text' => 'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE', 'reason' => 'Un service légitime ne redemande jamais un mot de passe par email'],
|
||||||
'ACTION OBLIGATOIRE DANS LES 5 MINUTES',
|
['text' => 'ACTION OBLIGATOIRE DANS LES 5 MINUTES', 'reason' => 'Délai artificiellement très court'],
|
||||||
'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE',
|
['text' => 'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE', 'reason' => 'Usurpation d’un organisme bancaire'],
|
||||||
'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT',
|
['text' => 'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT', 'reason' => 'Cadeau non sollicité'],
|
||||||
'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER',
|
['text' => 'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER', 'reason' => 'Peur infondée utilisée pour pousser à agir vite'],
|
||||||
'VOUS ÊTES LE GRAND GAGNANT DU JOUR',
|
['text' => 'VOUS ÊTES LE GRAND GAGNANT DU JOUR', 'reason' => 'Gain surprise non sollicité'],
|
||||||
'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE',
|
['text' => 'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE', 'reason' => 'Fausse alerte de paiement pour voler des données bancaires'],
|
||||||
'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE',
|
['text' => 'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE', 'reason' => 'Menace de fermeture de compte'],
|
||||||
'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER',
|
['text' => 'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER', 'reason' => 'Offre “secrète” artificiellement exclusive'],
|
||||||
'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT',
|
['text' => 'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT', 'reason' => 'Fausse urgence de session'],
|
||||||
'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI',
|
['text' => 'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI', 'reason' => 'Remboursement non sollicité'],
|
||||||
'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES',
|
['text' => 'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES', 'reason' => 'Compte à rebours anxiogène'],
|
||||||
'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE',
|
['text' => 'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE', 'reason' => 'Menace de suppression définitive'],
|
||||||
'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ',
|
['text' => 'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ', 'reason' => 'Fausse alerte de piratage'],
|
||||||
'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE',
|
['text' => 'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE', 'reason' => 'Demande d’informations bancaires par email'],
|
||||||
'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT'
|
['text' => 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT', 'reason' => 'Promesse d’argent contre des données personnelles'],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Liste des expéditeurs spam
|
// Liste des expéditeurs spam
|
||||||
@@ -219,6 +225,10 @@ function firstLetterSafe(string $text): string
|
|||||||
return strtoupper(substr($text, 0, 1));
|
return strtoupper(substr($text, 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construit une nouvelle série de mails pour une manche de captcha.
|
||||||
|
// Le nombre de mails frauduleux varie entre 1 et 3 : un robot ne peut donc pas se
|
||||||
|
// contenter de "toujours cocher une seule case au hasard", ce qui réduit fortement
|
||||||
|
// les chances de réussir par pure chance (voir le calcul dans la doc du projet).
|
||||||
function buildInboxRound(
|
function buildInboxRound(
|
||||||
array $normalSubjects,
|
array $normalSubjects,
|
||||||
array $normalSenders,
|
array $normalSenders,
|
||||||
@@ -236,18 +246,20 @@ function buildInboxRound(
|
|||||||
'subject' => $subject,
|
'subject' => $subject,
|
||||||
'preview' => $normalPreviews[array_rand($normalPreviews)],
|
'preview' => $normalPreviews[array_rand($normalPreviews)],
|
||||||
'is_spam' => false,
|
'is_spam' => false,
|
||||||
|
'reason' => null,
|
||||||
'time' => sprintf('%02d:%02d', random_int(8, 19), random_int(0, 59))
|
'time' => sprintf('%02d:%02d', random_int(8, 19), random_int(0, 59))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$spamPool = [];
|
$spamPool = [];
|
||||||
foreach ($spamSubjects as $index => $subject) {
|
foreach ($spamSubjects as $index => $spam) {
|
||||||
$spamPool[] = [
|
$spamPool[] = [
|
||||||
'id' => 'spam_' . $index,
|
'id' => 'spam_' . $index,
|
||||||
'sender' => $spamSenders[array_rand($spamSenders)],
|
'sender' => $spamSenders[array_rand($spamSenders)],
|
||||||
'subject' => $subject,
|
'subject' => $spam['text'],
|
||||||
'preview' => $spamPreviews[array_rand($spamPreviews)],
|
'preview' => $spamPreviews[array_rand($spamPreviews)],
|
||||||
'is_spam' => true,
|
'is_spam' => true,
|
||||||
|
'reason' => $spam['reason'],
|
||||||
'time' => sprintf('%02d:%02d', random_int(0, 23), random_int(0, 59))
|
'time' => sprintf('%02d:%02d', random_int(0, 23), random_int(0, 59))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -255,15 +267,25 @@ function buildInboxRound(
|
|||||||
shuffle($normalPool);
|
shuffle($normalPool);
|
||||||
shuffle($spamPool);
|
shuffle($spamPool);
|
||||||
|
|
||||||
$selected = array_slice($normalPool, 0, 9);
|
// Entre 1 et 3 mails frauduleux dans les 10 affichés (voir commentaire de la fonction)
|
||||||
$selected[] = $spamPool[0];
|
$spamCount = random_int(1, 3);
|
||||||
|
$selectedSpam = array_slice($spamPool, 0, $spamCount);
|
||||||
|
$selectedNormal = array_slice($normalPool, 0, 10 - $spamCount);
|
||||||
|
|
||||||
|
$selected = array_merge($selectedSpam, $selectedNormal);
|
||||||
shuffle($selected);
|
shuffle($selected);
|
||||||
|
|
||||||
|
$correctIds = array_map(function ($mail) {
|
||||||
|
return $mail['id'];
|
||||||
|
}, $selectedSpam);
|
||||||
|
sort($correctIds);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'token' => bin2hex(random_bytes(16)),
|
'token' => bin2hex(random_bytes(16)),
|
||||||
'correct_id' => $spamPool[0]['id'],
|
'correct_ids' => $correctIds,
|
||||||
'emails' => $selected,
|
'emails' => $selected,
|
||||||
'attempts_left' => $maxAttempts
|
'attempts_left' => $maxAttempts,
|
||||||
|
'started_at' => microtime(true)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,8 +305,15 @@ $round = $_SESSION['captcha_round'];
|
|||||||
$error = '';
|
$error = '';
|
||||||
$success = false;
|
$success = false;
|
||||||
$locked = false;
|
$locked = false;
|
||||||
|
$foundReasons = [];
|
||||||
|
|
||||||
if (!isset($round['emails']) || !is_array($round['emails'])) {
|
// Regénère une série si la structure en session est invalide ou date d'avant cette mise à
|
||||||
|
// jour (ancienne session sans correct_ids/started_at) : évite tout crash après déploiement
|
||||||
|
if (
|
||||||
|
!isset($round['emails']) || !is_array($round['emails']) ||
|
||||||
|
!isset($round['correct_ids']) || !is_array($round['correct_ids']) ||
|
||||||
|
!isset($round['started_at'])
|
||||||
|
) {
|
||||||
$_SESSION['captcha_round'] = buildInboxRound(
|
$_SESSION['captcha_round'] = buildInboxRound(
|
||||||
$normalSubjects,
|
$normalSubjects,
|
||||||
$normalSenders,
|
$normalSenders,
|
||||||
@@ -300,33 +329,66 @@ if (!isset($round['emails']) || !is_array($round['emails'])) {
|
|||||||
// Gestion de la soumission du formulaire
|
// Gestion de la soumission du formulaire
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$postedToken = $_POST['captcha_token'] ?? '';
|
$postedToken = $_POST['captcha_token'] ?? '';
|
||||||
$selectedMail = $_POST['selected_mail'] ?? '';
|
|
||||||
|
$selectedMails = (isset($_POST['selected_mails']) && is_array($_POST['selected_mails']))
|
||||||
|
? array_map('strval', $_POST['selected_mails'])
|
||||||
|
: [];
|
||||||
|
sort($selectedMails);
|
||||||
|
|
||||||
|
// Honeypot : champ invisible pour un humain (masqué en CSS), que les robots qui
|
||||||
|
// remplissent tous les champs d'un formulaire ont tendance à compléter malgré tout
|
||||||
|
$honeypot = trim($_POST['website'] ?? '');
|
||||||
|
|
||||||
if (!hash_equals($round['token'], $postedToken)) {
|
if (!hash_equals($round['token'], $postedToken)) {
|
||||||
$error = 'Session invalide, recharge la page.';
|
$error = 'Session invalide, recharge la page.';
|
||||||
} elseif (($round['attempts_left'] ?? 0) <= 0) {
|
} elseif (($round['attempts_left'] ?? 0) <= 0) {
|
||||||
$locked = true;
|
$locked = true;
|
||||||
$error = 'Tu n’as plus d’essais. Recharge une nouvelle série.';
|
$error = 'Tu n’as plus d’essais. Recharge une nouvelle série.';
|
||||||
} elseif ($selectedMail !== $round['correct_id']) {
|
} elseif ($honeypot !== '') {
|
||||||
|
// On fait échouer la tentative sans révéler la vraie raison, pour ne pas
|
||||||
|
// donner d'indice à un robot sur la façon dont il a été détecté
|
||||||
|
$_SESSION['captcha_round']['attempts_left']--;
|
||||||
|
$round = $_SESSION['captcha_round'];
|
||||||
|
$locked = ($round['attempts_left'] ?? 0) <= 0;
|
||||||
|
$error = $locked
|
||||||
|
? 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.'
|
||||||
|
: 'Ce n’est pas la bonne sélection. Regarde mieux les messages les plus agressifs ou trop urgents.';
|
||||||
|
sendLog('Captcha : champ piège rempli (robot probable)');
|
||||||
|
} elseif ((microtime(true) - (float) $round['started_at']) < $minSolveTime) {
|
||||||
|
// Une réponse aussi rapide n'est pas plausible pour un humain qui doit lire 10 mails
|
||||||
|
$_SESSION['captcha_round']['attempts_left']--;
|
||||||
|
$round = $_SESSION['captcha_round'];
|
||||||
|
$locked = ($round['attempts_left'] ?? 0) <= 0;
|
||||||
|
$error = $locked
|
||||||
|
? 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.'
|
||||||
|
: 'Ce n’est pas la bonne sélection. Prends le temps de lire les messages.';
|
||||||
|
sendLog('Captcha : soumission trop rapide (robot probable)');
|
||||||
|
} elseif ($selectedMails !== $round['correct_ids']) {
|
||||||
$_SESSION['captcha_round']['attempts_left']--;
|
$_SESSION['captcha_round']['attempts_left']--;
|
||||||
$round = $_SESSION['captcha_round'];
|
$round = $_SESSION['captcha_round'];
|
||||||
|
|
||||||
if (($round['attempts_left'] ?? 0) <= 0) {
|
if (($round['attempts_left'] ?? 0) <= 0) {
|
||||||
$locked = true;
|
$locked = true;
|
||||||
$error = 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.';
|
$error = 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.';
|
||||||
// Log Discord : visiteur bloqué au captcha (possible robot)
|
sendLog('Captcha raté : plus aucun essai restant (possible robot)');
|
||||||
sendLog("Captcha raté : plus aucun essai restant (possible robot)");
|
|
||||||
} else {
|
} else {
|
||||||
$error = 'Ce n’est pas le bon mail. Regarde mieux le message le plus agressif ou trop urgent.';
|
$error = 'Ce n’est pas la bonne sélection. Regarde mieux les messages les plus agressifs ou trop urgents.';
|
||||||
// Log Discord : mauvaise réponse au captcha
|
sendLog('Captcha raté : mauvaise sélection (' . $round['attempts_left'] . ' essai(s) restant(s))');
|
||||||
sendLog("Captcha raté : mauvaise réponse (" . $round['attempts_left'] . " essai(s) restant(s))");
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$_SESSION['captcha_valid'] = true;
|
$_SESSION['captcha_valid'] = true;
|
||||||
|
|
||||||
|
// On récupère les explications des mails trouvés pour les afficher sur
|
||||||
|
// l'écran de succès (petit bonus pédagogique) avant de vider la manche
|
||||||
|
foreach ($round['emails'] as $mail) {
|
||||||
|
if ($mail['is_spam'] && !empty($mail['reason'])) {
|
||||||
|
$foundReasons[] = $mail['reason'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unset($_SESSION['captcha_round']);
|
unset($_SESSION['captcha_round']);
|
||||||
$success = true;
|
$success = true;
|
||||||
// Log Discord : captcha validé, le visiteur accède au site
|
sendLog('Captcha réussi : nouveau visiteur sur le site');
|
||||||
sendLog("Captcha réussi : nouveau visiteur sur le site");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +404,7 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||||
<link rel="stylesheet" href="./styles/captcha.css">
|
<link rel="stylesheet" href="./styles/captcha.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="<?php echo $success ? 'success-mode' : ''; ?>">
|
<body class="<?php echo $success ? 'success-mode' : ''; ?>">
|
||||||
@@ -351,7 +414,19 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
|||||||
<div class="success-burst"></div>
|
<div class="success-burst"></div>
|
||||||
<div class="success-icon">✅</div>
|
<div class="success-icon">✅</div>
|
||||||
<h1>Vérification réussie</h1>
|
<h1>Vérification réussie</h1>
|
||||||
<p>Bravo, tu as trouvé le mail frauduleux. Redirection en cours...</p>
|
<p>Bravo, tu as repéré le(s) mail(s) frauduleux. Redirection en cours...</p>
|
||||||
|
|
||||||
|
<?php if (!empty($foundReasons)): ?>
|
||||||
|
<div class="success-reasons">
|
||||||
|
<p class="success-reasons-title">Ce que tu as repéré :</p>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($foundReasons as $reason): ?>
|
||||||
|
<li><i class="reason-dot"></i><?php echo htmlspecialchars($reason, ENT_QUOTES, 'UTF-8'); ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="success-loader">
|
<div class="success-loader">
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,7 +435,7 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
|||||||
<script>
|
<script>
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
window.location.href = './index.php';
|
window.location.href = './index.php';
|
||||||
}, 1800);
|
}, 2400);
|
||||||
</script>
|
</script>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<form class="captcha-card" method="post" action="">
|
<form class="captcha-card" method="post" action="">
|
||||||
@@ -370,13 +445,14 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
|||||||
<div>
|
<div>
|
||||||
<h1 class="captcha-title">Vérification humaine</h1>
|
<h1 class="captcha-title">Vérification humaine</h1>
|
||||||
<p class="captcha-subtitle">
|
<p class="captcha-subtitle">
|
||||||
Pour continuer, cliquez sur le <strong>mail frauduleux</strong> parmi les 10 messages ci-dessous.
|
Sélectionne <strong>tous les emails qui te semblent frauduleux</strong> parmi les messages
|
||||||
|
ci-dessous. Certains sont plus dangereux que d'autres.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="captcha-target">
|
<div class="captcha-target">
|
||||||
<span>Vérification demandée</span>
|
<span>Vérification demandée</span>
|
||||||
<strong>Trouver le spam</strong>
|
<strong>Repérer le phishing</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -384,6 +460,11 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
|||||||
<div class="captcha-attempts">
|
<div class="captcha-attempts">
|
||||||
<span>Essais restants</span>
|
<span>Essais restants</span>
|
||||||
<strong><?php echo htmlspecialchars((string) $attemptsLeft, ENT_QUOTES, 'UTF-8'); ?> / <?php echo htmlspecialchars((string) $maxAttempts, ENT_QUOTES, 'UTF-8'); ?></strong>
|
<strong><?php echo htmlspecialchars((string) $attemptsLeft, ENT_QUOTES, 'UTF-8'); ?> / <?php echo htmlspecialchars((string) $maxAttempts, ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||||
|
<div class="attempts-dots">
|
||||||
|
<?php for ($i = 0; $i < $maxAttempts; $i++): ?>
|
||||||
|
<span class="<?php echo $i < $attemptsLeft ? 'dot-filled' : 'dot-empty'; ?>"></span>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($locked): ?>
|
<?php if ($locked): ?>
|
||||||
@@ -402,19 +483,21 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
|||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mailbox-label">Test anti-robot : trouvez le mail qui ressemble à une arnaque</div>
|
<div class="mailbox-label">Test anti-robot : cochez les mails qui ressemblent à une arnaque</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mail-list">
|
<div class="mail-list">
|
||||||
<?php foreach ($round['emails'] as $mail): ?>
|
<?php foreach ($round['emails'] as $index => $mail): ?>
|
||||||
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>">
|
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>" style="--mail-index: <?php echo (int) $index; ?>">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="checkbox"
|
||||||
name="selected_mail"
|
name="selected_mails[]"
|
||||||
value="<?php echo htmlspecialchars($mail['id'], ENT_QUOTES, 'UTF-8'); ?>"
|
value="<?php echo htmlspecialchars($mail['id'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
<?php echo $locked ? 'disabled' : 'required'; ?>
|
<?php echo $locked ? 'disabled' : ''; ?>
|
||||||
>
|
>
|
||||||
<span class="mail-row">
|
<span class="mail-row">
|
||||||
|
<span class="mail-check"><i class="fa-solid fa-check"></i></span>
|
||||||
|
|
||||||
<span class="mail-avatar">
|
<span class="mail-avatar">
|
||||||
<?php echo htmlspecialchars(firstLetterSafe($mail['sender']), ENT_QUOTES, 'UTF-8'); ?>
|
<?php echo htmlspecialchars(firstLetterSafe($mail['sender']), ENT_QUOTES, 'UTF-8'); ?>
|
||||||
</span>
|
</span>
|
||||||
@@ -447,6 +530,13 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
|||||||
value="<?php echo htmlspecialchars($round['token'], ENT_QUOTES, 'UTF-8'); ?>"
|
value="<?php echo htmlspecialchars($round['token'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- Champ piège (honeypot) : invisible et exclu du tabulateur pour un humain,
|
||||||
|
mais souvent rempli automatiquement par les robots qui remplissent tout -->
|
||||||
|
<div class="captcha-honeypot" aria-hidden="true">
|
||||||
|
<label for="website">Laisse ce champ vide</label>
|
||||||
|
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="captcha-footer">
|
<div class="captcha-footer">
|
||||||
<p class="captcha-note">
|
<p class="captcha-note">
|
||||||
Tu as plusieurs essais. Les faux mails utilisent souvent l’urgence, les cadeaux improbables, les menaces ou les demandes de mot de passe.
|
Tu as plusieurs essais. Les faux mails utilisent souvent l’urgence, les cadeaux improbables, les menaces ou les demandes de mot de passe.
|
||||||
|
|||||||
+202
-32
@@ -1,7 +1,7 @@
|
|||||||
body {
|
body {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top, rgba(108, 99, 255, 0.18), transparent 35%),
|
radial-gradient(circle at top, rgba(255, 122, 26, 0.16), transparent 35%),
|
||||||
#0d0d0d;
|
#0a0a10;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
|
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -21,10 +21,22 @@ body {
|
|||||||
.captcha-card,
|
.captcha-card,
|
||||||
.success-screen {
|
.success-screen {
|
||||||
background: linear-gradient(180deg, #111116 0%, #161622 100%);
|
background: linear-gradient(180deg, #111116 0%, #161622 100%);
|
||||||
border: 1px solid #2a2a3e;
|
border: 1px solid #262638;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
|
animation: cardEnter 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cardEnter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-badge {
|
.captcha-badge {
|
||||||
@@ -33,9 +45,9 @@ body {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(108, 99, 255, 0.14);
|
background: rgba(255, 122, 26, 0.14);
|
||||||
border: 1px solid rgba(108, 99, 255, 0.28);
|
border: 1px solid rgba(255, 122, 26, 0.3);
|
||||||
color: #9d97ff;
|
color: #ffa155;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
@@ -63,10 +75,14 @@ body {
|
|||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captcha-subtitle strong {
|
||||||
|
color: #ffa155;
|
||||||
|
}
|
||||||
|
|
||||||
.captcha-target {
|
.captcha-target {
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
background-color: #1a1a24;
|
background-color: #1a1a24;
|
||||||
border: 1px solid #2a2a3e;
|
border: 1px solid #262638;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
@@ -82,7 +98,7 @@ body {
|
|||||||
|
|
||||||
.captcha-target strong {
|
.captcha-target strong {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
color: #8f88ff;
|
color: #ffa155;
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-stats {
|
.captcha-stats {
|
||||||
@@ -95,6 +111,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.captcha-attempts {
|
.captcha-attempts {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
background-color: #171b28;
|
background-color: #171b28;
|
||||||
border: 1px solid #283049;
|
border: 1px solid #283049;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -115,6 +134,28 @@ body {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pastilles visuelles représentant les essais restants */
|
||||||
|
.attempts-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attempts-dots span {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attempts-dots .dot-filled {
|
||||||
|
background: linear-gradient(180deg, #ff7a1a 0%, #e8630a 100%);
|
||||||
|
box-shadow: 0 0 8px rgba(255, 122, 26, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attempts-dots .dot-empty {
|
||||||
|
background-color: #2a2f42;
|
||||||
|
}
|
||||||
|
|
||||||
.captcha-reset,
|
.captcha-reset,
|
||||||
.captcha-secondary-button {
|
.captcha-secondary-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -124,14 +165,15 @@ body {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #2a314a;
|
background-color: #2a2a3a;
|
||||||
border: 1px solid #3d4767;
|
border: 1px solid #3a3a4e;
|
||||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
transition: transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-reset:hover,
|
.captcha-reset:hover,
|
||||||
.captcha-secondary-button:hover {
|
.captcha-secondary-button:hover {
|
||||||
background-color: #394362;
|
background-color: #34344a;
|
||||||
|
border-color: rgba(255, 122, 26, 0.4);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +185,15 @@ body {
|
|||||||
color: #ffb8bb;
|
color: #ffb8bb;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
animation: errorShake 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes errorShake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
20% { transform: translateX(-6px); }
|
||||||
|
40% { transform: translateX(5px); }
|
||||||
|
60% { transform: translateX(-4px); }
|
||||||
|
80% { transform: translateX(3px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox {
|
.mailbox {
|
||||||
@@ -199,10 +250,25 @@ body {
|
|||||||
|
|
||||||
.mail-option {
|
.mail-option {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
opacity: 0;
|
||||||
|
animation: mailEnter 0.4s ease forwards;
|
||||||
|
animation-delay: calc(var(--mail-index, 0) * 0.04s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mailEnter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-option-disabled {
|
.mail-option-disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-option input {
|
.mail-option input {
|
||||||
@@ -211,9 +277,15 @@ body {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Anneau de focus visible au clavier (accessibilité) sur la ligne associée à la checkbox */
|
||||||
|
.mail-option input:focus-visible + .mail-row {
|
||||||
|
outline: 2px solid #ff7a1a;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
.mail-row {
|
.mail-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 48px minmax(120px, 220px) 1fr 70px;
|
grid-template-columns: 26px 48px minmax(120px, 220px) 1fr 70px;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
@@ -232,16 +304,38 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mail-option input:checked + .mail-row {
|
.mail-option input:checked + .mail-row {
|
||||||
background-color: rgba(108, 99, 255, 0.14);
|
background-color: rgba(255, 122, 26, 0.12);
|
||||||
box-shadow: inset 0 0 0 1px rgba(108, 99, 255, 0.35);
|
box-shadow: inset 0 0 0 1px rgba(255, 122, 26, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case à cocher personnalisée */
|
||||||
|
.mail-check {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #1a1a24;
|
||||||
|
border: 1px solid #3a3a4e;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-option input:checked + .mail-row .mail-check {
|
||||||
|
background: linear-gradient(180deg, #ff7a1a 0%, #e8630a 100%);
|
||||||
|
border-color: #ff7a1a;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-avatar {
|
.mail-avatar {
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(180deg, #2f3550 0%, #1f2334 100%);
|
background: linear-gradient(180deg, #2a2a3a 0%, #1a1a24 100%);
|
||||||
border: 1px solid #303650;
|
border: 1px solid #303040;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -287,6 +381,16 @@ body {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Champ piège (honeypot) : invisible pour un humain, exclu du tabulateur,
|
||||||
|
mais toujours présent dans le HTML pour les robots qui remplissent tout */
|
||||||
|
.captcha-honeypot {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.captcha-footer {
|
.captcha-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -308,7 +412,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.captcha-submit {
|
.captcha-submit {
|
||||||
background: linear-gradient(180deg, #7a72ff 0%, #5e56eb 100%);
|
background: linear-gradient(180deg, #ff8c3a 0%, #e8630a 100%);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -317,12 +421,16 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
box-shadow: 0 12px 24px rgba(94, 86, 235, 0.28);
|
box-shadow: 0 12px 24px rgba(232, 99, 10, 0.32);
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-submit:hover {
|
.captcha-submit:hover {
|
||||||
transform: translateY(-1px) scale(1.01);
|
transform: translateY(-1px) scale(1.01);
|
||||||
box-shadow: 0 16px 30px rgba(94, 86, 235, 0.34);
|
box-shadow: 0 16px 30px rgba(232, 99, 10, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-submit:active {
|
||||||
|
transform: translateY(1px) scale(0.99);
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-mode {
|
.success-mode {
|
||||||
@@ -348,12 +456,60 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.success-screen p {
|
.success-screen p {
|
||||||
margin: 0 auto 24px;
|
margin: 0 auto 12px;
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
color: #ced2e6;
|
color: #ced2e6;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bloc pédagogique listant les indices de phishing repérés */
|
||||||
|
.success-reasons {
|
||||||
|
max-width: 460px;
|
||||||
|
margin: 18px auto 26px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
text-align: left;
|
||||||
|
background: rgba(255, 122, 26, 0.08);
|
||||||
|
border: 1px solid rgba(255, 122, 26, 0.25);
|
||||||
|
border-radius: 16px;
|
||||||
|
animation: cardEnter 0.5s ease 0.3s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-reasons-title {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #ffa155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-reasons ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-reasons li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
color: #d8d9e6;
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
margin-top: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff8c3a;
|
||||||
|
}
|
||||||
|
|
||||||
.success-loader {
|
.success-loader {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
@@ -368,8 +524,8 @@ body {
|
|||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0;
|
width: 0;
|
||||||
background: linear-gradient(90deg, #62d26f 0%, #9cffaf 100%);
|
background: linear-gradient(90deg, #ffa155 0%, #ff7a1a 100%);
|
||||||
animation: loadingBar 1.8s linear forwards;
|
animation: loadingBar 2.4s linear forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-burst {
|
.success-burst {
|
||||||
@@ -377,10 +533,10 @@ body {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 20% 30%, rgba(255, 215, 0, 0.25), transparent 18%),
|
radial-gradient(circle at 20% 30%, rgba(255, 197, 92, 0.25), transparent 18%),
|
||||||
radial-gradient(circle at 80% 20%, rgba(98, 210, 111, 0.22), transparent 18%),
|
radial-gradient(circle at 80% 20%, rgba(98, 210, 111, 0.2), transparent 18%),
|
||||||
radial-gradient(circle at 30% 80%, rgba(122, 114, 255, 0.2), transparent 20%),
|
radial-gradient(circle at 30% 80%, rgba(255, 122, 26, 0.22), transparent 20%),
|
||||||
radial-gradient(circle at 75% 70%, rgba(255, 105, 180, 0.16), transparent 18%);
|
radial-gradient(circle at 75% 70%, rgba(255, 145, 77, 0.18), transparent 18%);
|
||||||
animation: burstPulse 1s ease-in-out infinite alternate;
|
animation: burstPulse 1s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +585,22 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.captcha-card,
|
||||||
|
.success-screen,
|
||||||
|
.mail-option,
|
||||||
|
.success-reasons,
|
||||||
|
.success-icon,
|
||||||
|
.captcha-error {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-burst {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.captcha-top {
|
.captcha-top {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -440,7 +612,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mail-row {
|
.mail-row {
|
||||||
grid-template-columns: 42px 1fr 56px;
|
grid-template-columns: 22px 42px 1fr 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-sender {
|
.mail-sender {
|
||||||
@@ -482,15 +654,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mail-row {
|
.mail-row {
|
||||||
grid-template-columns: 36px 1fr;
|
grid-template-columns: 20px 36px 1fr;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 14px 14px;
|
padding: 14px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-time {
|
.mail-time {
|
||||||
grid-column: 2;
|
display: none;
|
||||||
text-align: left;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-screen h1 {
|
.success-screen h1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user