Compare commits
18
Commits
9bbc05bcac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c5ce36608 | ||
|
|
376a20966f | ||
|
|
d47258fad8 | ||
|
|
d7dc48454c | ||
|
|
6164f7eaae | ||
|
|
542910a15d | ||
|
|
a19c5c8381 | ||
|
|
88c10ee5b0 | ||
|
|
c2400d27b7 | ||
|
|
b5bacf04a9 | ||
|
|
87cc4e5e9c | ||
|
|
97caf2275c | ||
|
|
481fbc9498 | ||
|
|
6fb78e0bbb | ||
|
|
ba9bb21953 | ||
|
|
07bae3eff3 | ||
|
|
a019dcb00d | ||
|
|
e50eeacaeb |
+15
@@ -0,0 +1,15 @@
|
||||
# Config Claude Code
|
||||
.claude/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Éditeurs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
+157
-51
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/functions/security.php');
|
||||
|
||||
// Ajout des logs Discord (chemin via __DIR__ car captcha.php est à la racine du site)
|
||||
require_once(__DIR__ . '/functions/sendLogs.php');
|
||||
|
||||
// Recharger une nouvelle série si demandé
|
||||
if (isset($_GET['reload']) && $_GET['reload'] === '1') {
|
||||
@@ -17,6 +20,11 @@ if (isset($_SESSION['captcha_valid']) && $_SESSION['captcha_valid'] === true) {
|
||||
// Nombre maximum d'essais
|
||||
$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
|
||||
$normalSubjects = [
|
||||
'Votre rendez-vous est confirmé',
|
||||
@@ -147,40 +155,44 @@ $normalSenders = [
|
||||
'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 = [
|
||||
'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURD’HUI',
|
||||
'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT',
|
||||
'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE',
|
||||
'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE',
|
||||
'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE',
|
||||
'ACTION OBLIGATOIRE DANS LES 5 MINUTES',
|
||||
'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE',
|
||||
'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT',
|
||||
'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER',
|
||||
'VOUS ÊTES LE GRAND GAGNANT DU JOUR',
|
||||
'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE',
|
||||
'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE',
|
||||
'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER',
|
||||
'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT',
|
||||
'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI',
|
||||
'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES',
|
||||
'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE',
|
||||
'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ',
|
||||
'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE',
|
||||
'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT'
|
||||
['text' => 'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURD’HUI', 'reason' => 'Urgence exagérée et abus des majuscules'],
|
||||
['text' => 'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT', 'reason' => 'Gain surprise, trop beau pour être vrai'],
|
||||
['text' => 'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE', 'reason' => 'Appel à l’action pressant et menaçant'],
|
||||
['text' => 'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE', 'reason' => 'Demande de paiement immédiat inhabituelle'],
|
||||
['text' => 'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE', 'reason' => 'Un service légitime ne redemande jamais un mot de passe par email'],
|
||||
['text' => 'ACTION OBLIGATOIRE DANS LES 5 MINUTES', 'reason' => 'Délai artificiellement très court'],
|
||||
['text' => 'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE', 'reason' => 'Usurpation d’un organisme bancaire'],
|
||||
['text' => 'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT', 'reason' => 'Cadeau non sollicité'],
|
||||
['text' => 'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER', 'reason' => 'Peur infondée utilisée pour pousser à agir vite'],
|
||||
['text' => 'VOUS ÊTES LE GRAND GAGNANT DU JOUR', 'reason' => 'Gain surprise non sollicité'],
|
||||
['text' => 'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE', 'reason' => 'Fausse alerte de paiement pour voler des données bancaires'],
|
||||
['text' => 'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE', 'reason' => 'Menace de fermeture de compte'],
|
||||
['text' => 'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER', 'reason' => 'Offre “secrète” artificiellement exclusive'],
|
||||
['text' => 'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT', 'reason' => 'Fausse urgence de session'],
|
||||
['text' => 'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI', 'reason' => 'Remboursement non sollicité'],
|
||||
['text' => 'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES', 'reason' => 'Compte à rebours anxiogène'],
|
||||
['text' => 'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE', 'reason' => 'Menace de suppression définitive'],
|
||||
['text' => 'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ', 'reason' => 'Fausse alerte de piratage'],
|
||||
['text' => 'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE', 'reason' => 'Demande d’informations bancaires par email'],
|
||||
['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 : mêmes formats d'affichage (nom, pas d'adresse brute) que les
|
||||
// expéditeurs normaux ci-dessus, pour qu'il ne soit pas possible de repérer les mails
|
||||
// frauduleux d'un simple coup d'œil sur l'expéditeur. La seule façon de les distinguer doit
|
||||
// être de lire le contenu (sujet), comme un vrai email de phishing usurpant une identité connue.
|
||||
$spamSenders = [
|
||||
'service-urgent@compte-bloque-now.biz',
|
||||
'cadeau-gratuit@super-offre-win.click',
|
||||
'banque-alerte@verification-securite.top',
|
||||
'colis-stop@paiement-obligatoire.help',
|
||||
'admin-compte@action-immediate.world',
|
||||
'support-prime@argent-rapide-fast.support',
|
||||
'verification@motdepasse-danger.win',
|
||||
'alerte@compte-fermeture.click'
|
||||
'Sécurité du compte',
|
||||
'Service client',
|
||||
'Banque familiale',
|
||||
'Livraison Colis',
|
||||
'Support technique',
|
||||
'Service facturation',
|
||||
'Assurance Habitation',
|
||||
'Mutuelle Santé'
|
||||
];
|
||||
|
||||
// Liste des aperçus normaux
|
||||
@@ -216,6 +228,10 @@ function firstLetterSafe(string $text): string
|
||||
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(
|
||||
array $normalSubjects,
|
||||
array $normalSenders,
|
||||
@@ -233,18 +249,20 @@ function buildInboxRound(
|
||||
'subject' => $subject,
|
||||
'preview' => $normalPreviews[array_rand($normalPreviews)],
|
||||
'is_spam' => false,
|
||||
'reason' => null,
|
||||
'time' => sprintf('%02d:%02d', random_int(8, 19), random_int(0, 59))
|
||||
];
|
||||
}
|
||||
|
||||
$spamPool = [];
|
||||
foreach ($spamSubjects as $index => $subject) {
|
||||
foreach ($spamSubjects as $index => $spam) {
|
||||
$spamPool[] = [
|
||||
'id' => 'spam_' . $index,
|
||||
'sender' => $spamSenders[array_rand($spamSenders)],
|
||||
'subject' => $subject,
|
||||
'subject' => $spam['text'],
|
||||
'preview' => $spamPreviews[array_rand($spamPreviews)],
|
||||
'is_spam' => true,
|
||||
'reason' => $spam['reason'],
|
||||
'time' => sprintf('%02d:%02d', random_int(0, 23), random_int(0, 59))
|
||||
];
|
||||
}
|
||||
@@ -252,15 +270,25 @@ function buildInboxRound(
|
||||
shuffle($normalPool);
|
||||
shuffle($spamPool);
|
||||
|
||||
$selected = array_slice($normalPool, 0, 9);
|
||||
$selected[] = $spamPool[0];
|
||||
// Entre 1 et 3 mails frauduleux dans les 10 affichés (voir commentaire de la fonction)
|
||||
$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);
|
||||
|
||||
$correctIds = array_map(function ($mail) {
|
||||
return $mail['id'];
|
||||
}, $selectedSpam);
|
||||
sort($correctIds);
|
||||
|
||||
return [
|
||||
'token' => bin2hex(random_bytes(16)),
|
||||
'correct_id' => $spamPool[0]['id'],
|
||||
'correct_ids' => $correctIds,
|
||||
'emails' => $selected,
|
||||
'attempts_left' => $maxAttempts
|
||||
'attempts_left' => $maxAttempts,
|
||||
'started_at' => microtime(true)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -280,8 +308,15 @@ $round = $_SESSION['captcha_round'];
|
||||
$error = '';
|
||||
$success = 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(
|
||||
$normalSubjects,
|
||||
$normalSenders,
|
||||
@@ -297,27 +332,66 @@ if (!isset($round['emails']) || !is_array($round['emails'])) {
|
||||
// Gestion de la soumission du formulaire
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$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)) {
|
||||
$error = 'Session invalide, recharge la page.';
|
||||
} elseif (($round['attempts_left'] ?? 0) <= 0) {
|
||||
$locked = true;
|
||||
$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']--;
|
||||
$round = $_SESSION['captcha_round'];
|
||||
|
||||
if (($round['attempts_left'] ?? 0) <= 0) {
|
||||
$locked = true;
|
||||
$error = 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.';
|
||||
sendLog('Captcha raté : plus aucun essai restant (possible robot)');
|
||||
} 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.';
|
||||
sendLog('Captcha raté : mauvaise sélection (' . $round['attempts_left'] . ' essai(s) restant(s))');
|
||||
}
|
||||
} else {
|
||||
$_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']);
|
||||
$success = true;
|
||||
sendLog('Captcha réussi : nouveau visiteur sur le site');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +403,11 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test anti-robot</title>
|
||||
<link rel="icon" type="image/png" href="./memoji.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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 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">
|
||||
</head>
|
||||
<body class="<?php echo $success ? 'success-mode' : ''; ?>">
|
||||
@@ -338,7 +417,19 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
||||
<div class="success-burst"></div>
|
||||
<div class="success-icon">✅</div>
|
||||
<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">
|
||||
<span></span>
|
||||
</div>
|
||||
@@ -347,7 +438,7 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
||||
<script>
|
||||
setTimeout(function () {
|
||||
window.location.href = './index.php';
|
||||
}, 1800);
|
||||
}, 2400);
|
||||
</script>
|
||||
<?php else: ?>
|
||||
<form class="captcha-card" method="post" action="">
|
||||
@@ -357,13 +448,14 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
||||
<div>
|
||||
<h1 class="captcha-title">Vérification humaine</h1>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="captcha-target">
|
||||
<span>Vérification demandée</span>
|
||||
<strong>Trouver le spam</strong>
|
||||
<strong>Repérer le phishing</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -371,6 +463,11 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
||||
<div class="captcha-attempts">
|
||||
<span>Essais restants</span>
|
||||
<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>
|
||||
|
||||
<?php if ($locked): ?>
|
||||
@@ -389,19 +486,21 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
||||
<span></span>
|
||||
<span></span>
|
||||
</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 class="mail-list">
|
||||
<?php foreach ($round['emails'] as $mail): ?>
|
||||
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>">
|
||||
<?php foreach ($round['emails'] as $index => $mail): ?>
|
||||
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>" style="--mail-index: <?php echo (int) $index; ?>">
|
||||
<input
|
||||
type="radio"
|
||||
name="selected_mail"
|
||||
type="checkbox"
|
||||
name="selected_mails[]"
|
||||
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-check"><i class="fa-solid fa-check"></i></span>
|
||||
|
||||
<span class="mail-avatar">
|
||||
<?php echo htmlspecialchars(firstLetterSafe($mail['sender']), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</span>
|
||||
@@ -434,6 +533,13 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
||||
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">
|
||||
<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.
|
||||
|
||||
+104
-22
@@ -1,16 +1,11 @@
|
||||
-- On créer la base de donnée si elle n'existe déjà aps
|
||||
CREATE DATABASE IF NOT EXISTS portfolio
|
||||
CREATE DATABASE IF NOT EXISTS portfolio;
|
||||
USE portfolio;
|
||||
|
||||
-- On créer un utilisateur avec un mot de passe fort qui est à changer après execution dus script (voir bitwarden)
|
||||
CREATE USER 'srv_portfolio'@'%' IDENTIFIED BY 'mdp';
|
||||
|
||||
-- On met les droits au compte sur la base portfolio à l'utilisateur
|
||||
-- Mot de passe à changer après exécution du script (voir Bitwarden)
|
||||
CREATE USER IF NOT EXISTS 'srv_portfolio'@'%' IDENTIFIED BY 'mdp';
|
||||
GRANT ALL ON portfolio.* TO 'srv_portfolio'@'%';
|
||||
|
||||
-- On actualise les permissions
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
-- Création de la table user : (Généré par Beekeeper Studio : équivalent à dbGate)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
@@ -18,43 +13,130 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
|
||||
created_at DATETIME NULL DEFAULT NULL,
|
||||
last_login DATETIME NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX idx_username (username)
|
||||
);
|
||||
|
||||
-- Création de la table projects : (Généré par Beekeeper Studio)
|
||||
INSERT IGNORE INTO users (id, created_at, last_login, password, role, username)
|
||||
VALUES (11, '2026-06-08 11:10:21', '2026-06-08 13:10:41', '$2y$12$x01fDqrHQEYIyj7MDHa72eRyl2j/n5cNq9sv3.FIiQCaRsB50Q4iK', 'admin', 'admin');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(100) NULL,
|
||||
tags VARCHAR(255) NULL,
|
||||
description TEXT NOT NULL,
|
||||
image_url VARCHAR(255) NOT NULL,
|
||||
markdown LONGTEXT NULL,
|
||||
link_url VARCHAR(255) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Création de la table projects : (Généré par Beekeeper Studio)
|
||||
CREATE TABLE IF NOT EXISTS experiences (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(100) NULL,
|
||||
xptype VARCHAR(100) NOT NULL,
|
||||
tags VARCHAR(255) NULL,
|
||||
description TEXT NOT NULL,
|
||||
duration VARCHAR(100) NOT NULL,
|
||||
image_url VARCHAR(255) NOT NULL,
|
||||
markdown LONGTEXT NULL,
|
||||
link_url VARCHAR(255) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Compte de test (PLUS A JOUR METTRE UN HASH)
|
||||
-- INSERT INTO users (username, password, role)
|
||||
-- VALUES ('admin', 'admin', 'admin');
|
||||
insert into `users` (`created_at`, `id`, `last_login`, `password`, `role`, `username`)
|
||||
values ('2026-06-08 11:10:21', 11, '2026-06-08 13:10:41', '$2y$12$x01fDqrHQEYIyj7MDHa72eRyl2j/n5cNq9sv3.FIiQCaRsB50Q4iK', 'admin', 'admin');
|
||||
CREATE TABLE IF NOT EXISTS schools (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(100) NULL,
|
||||
tags VARCHAR(255) NULL,
|
||||
description TEXT NOT NULL,
|
||||
duration VARCHAR(100) NOT NULL,
|
||||
image_url VARCHAR(255) NOT NULL,
|
||||
markdown LONGTEXT NULL,
|
||||
link_url VARCHAR(255) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Ajout d'une collonne status pour les projets et les expériences pour pouvoir différencier les projets/expériences en cours, terminés ou à venir
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN status VARCHAR(100) NULL AFTER title;
|
||||
ALTER TABLE experiences
|
||||
ADD COLUMN status VARCHAR(100) NULL AFTER title;
|
||||
-- Réglages du site (clé / valeur), utilisés pour la notification de la page d'accueil et le captcha
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
name VARCHAR(50) NOT NULL,
|
||||
value TEXT NULL,
|
||||
PRIMARY KEY (name)
|
||||
);
|
||||
|
||||
INSERT INTO settings (name, value) VALUES
|
||||
('site_notice_title', 'Des bugs peuvent encore se produire.'),
|
||||
('site_notice_text', 'Si le CSS ou les images s''affichent mal, pensez à vider le cache de votre navigateur (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications.'),
|
||||
('site_notice_enabled', '1'),
|
||||
('captcha_enabled', '1'),
|
||||
('contact_email', 'esteban.moussu@ik.me'),
|
||||
('contact_email_enabled', '1'),
|
||||
('contact_address_line1', '20 Grande rue'),
|
||||
('contact_address_line2', '52500 Broncourt'),
|
||||
('contact_address_enabled', '1')
|
||||
ON DUPLICATE KEY UPDATE value = VALUES(value);
|
||||
|
||||
-- items : un item par ligne, au format "classes-icone|Texte" (icône optionnelle)
|
||||
CREATE TABLE IF NOT EXISTS skills (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
icon VARCHAR(100) NULL,
|
||||
items TEXT NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
INSERT INTO skills (title, icon, items, position) VALUES
|
||||
('Systèmes d''exploitation', 'fa-solid fa-computer', 'fa-brands fa-microsoft|Windows (Serveur/XP/8/10/11)\nfa-brands fa-linux|Linux (Debian/Ubuntu)\nfa-brands fa-apple|macOS', 1),
|
||||
('Réseaux', 'fa-solid fa-network-wired', 'fa-solid fa-location-dot|Adressage IP\nfa-solid fa-diagram-project|VLAN\nfa-solid fa-shield-halved|Pare-feu (iptables/ufw)\nfa-solid fa-globe|DNS (Bind9)\nfa-solid fa-server|DHCP (ISC DHCP)\nfa-solid fa-arrows-left-right|Proxy (Squid)', 2),
|
||||
('Scripting', 'fa-solid fa-terminal', 'fa-solid fa-terminal|Bash\nfa-brands fa-python|Python\nfa-brands fa-java|Java\nfa-solid fa-gears|Ansible', 3),
|
||||
('Applications', 'fa-solid fa-cubes', 'fa-solid fa-address-book|LDAP (Windows AD)\nfa-solid fa-headset|GLPI\nfa-solid fa-compact-disc|FOG\nfa-solid fa-server|Nginx / Apache\nfa-brands fa-docker|Docker\nfa-solid fa-display|Guacamole\nfa-solid fa-database|MySQL / MariaDB', 4),
|
||||
('Développement web', 'fa-solid fa-code', 'fa-brands fa-html5|HTML\nfa-brands fa-css3-alt|CSS\nfa-brands fa-js|JavaScript\nfa-brands fa-php|PHP', 5),
|
||||
('Virtualisation & Outils', 'fa-solid fa-layer-group', 'fa-solid fa-server|Proxmox\nfa-solid fa-box|VirtualBox\nfa-brands fa-git-alt|Git (GitHub, Gitea)', 6);
|
||||
|
||||
-- category : 'tool' (bandeau "Outils que je sais manipuler") ou 'partner' (bandeau "Ils m'ont fait confiance")
|
||||
-- icon : classes CSS Devicon, prioritaire sur logo_url si les deux sont renseignés (au moins un des deux requis)
|
||||
CREATE TABLE IF NOT EXISTS logo_banners (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
category ENUM('tool', 'partner') NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
icon VARCHAR(100) NULL,
|
||||
logo_url VARCHAR(255) NULL,
|
||||
link_url VARCHAR(255) NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- PDF de projects/experiences/schools : tables associées plutôt qu'une colonne sur la table
|
||||
-- parente, pour que les pages qui LISTENT ces éléments n'aient jamais à charger le contenu
|
||||
-- binaire des PDF en mémoire (seul le nom de fichier y est utilisé)
|
||||
CREATE TABLE IF NOT EXISTS project_pdfs (
|
||||
project_id INT UNSIGNED NOT NULL,
|
||||
pdf_filename VARCHAR(255) NOT NULL,
|
||||
pdf_data LONGBLOB NOT NULL,
|
||||
PRIMARY KEY (project_id),
|
||||
CONSTRAINT fk_project_pdfs_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS experience_pdfs (
|
||||
experience_id INT UNSIGNED NOT NULL,
|
||||
pdf_filename VARCHAR(255) NOT NULL,
|
||||
pdf_data LONGBLOB NOT NULL,
|
||||
PRIMARY KEY (experience_id),
|
||||
CONSTRAINT fk_experience_pdfs_experience FOREIGN KEY (experience_id) REFERENCES experiences(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_pdfs (
|
||||
school_id INT UNSIGNED NOT NULL,
|
||||
pdf_filename VARCHAR(255) NOT NULL,
|
||||
pdf_data LONGBLOB NOT NULL,
|
||||
PRIMARY KEY (school_id),
|
||||
CONSTRAINT fk_school_pdfs_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
; Config PHP pour le conteneur Docker (image php:8.2-apache)
|
||||
; Copiée dans /usr/local/etc/php/conf.d/ par le Dockerfile généré par scriptsite.sh
|
||||
|
||||
; Augmente les limites d'upload pour permettre le dépôt de PDF depuis le panel admin
|
||||
; post_max_size doit toujours être >= upload_max_filesize
|
||||
upload_max_filesize = 20M
|
||||
post_max_size = 20M
|
||||
|
||||
; Coupe l'affichage des erreurs PHP dans le HTML en production (sécurité : ne pas exposer
|
||||
; les chemins serveur aux visiteurs) et les envoie dans les logs Apache/Docker à la place.
|
||||
; Bonus : évite les warnings HTML qui provoquaient "headers already sent" une fois affichés
|
||||
; avant le session_start()/header() des scripts.
|
||||
display_errors = Off
|
||||
log_errors = On
|
||||
+59
-2
@@ -1,9 +1,30 @@
|
||||
<?php
|
||||
// Ce Footer sera inseré sur des pages grâce a la fonction require_once('./../elements/footer.php')
|
||||
// Ce Footer sera inseré sur des pages grâce a la fonction require_once('./../elements/footer.php')
|
||||
// Rappel des différentes pages : Acceuil, Projets, Contact
|
||||
|
||||
// Notifications "toast" : affichées selon le paramètre présent dans l'URL
|
||||
// (?success=1 après un message de contact, ?login=1 après connexion, ?logout=1 après déconnexion, ?import=1 après un import)
|
||||
$toast = null;
|
||||
if (isset($_GET['success']) && $_GET['success'] === '1') {
|
||||
$toast = 'Votre message a bien été envoyé !';
|
||||
} elseif (isset($_GET['login']) && $_GET['login'] === '1') {
|
||||
$toast = 'Connexion réussie' . (isset($_SESSION['username']) ? ', bonjour ' . htmlspecialchars($_SESSION['username']) . ' !' : ' !');
|
||||
} elseif (isset($_GET['logout']) && $_GET['logout'] === '1') {
|
||||
$toast = 'Vous avez bien été déconnecté.';
|
||||
} elseif (isset($_GET['import']) && $_GET['import'] === '1') {
|
||||
$toast = 'Les données ont bien été importées.';
|
||||
}
|
||||
?>
|
||||
</main>
|
||||
|
||||
<?php if ($toast !== null): ?>
|
||||
<!-- Notification toast : disparaît toute seule, clic pour fermer -->
|
||||
<div class="toast" onclick="this.remove()" role="status">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span><?php echo $toast; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-content">
|
||||
<p class="footer-copyright">
|
||||
@@ -18,4 +39,40 @@
|
||||
<a href="../pages/cgu.php">CGU</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</footer>
|
||||
|
||||
<!-- Apparition des cartes au défilement (IntersectionObserver) -->
|
||||
<script>
|
||||
(function () {
|
||||
// On respecte la préférence "réduire les animations" du système
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
if (!('IntersectionObserver' in window)) return;
|
||||
|
||||
// Éléments qui apparaissent en glissant vers le haut quand ils entrent dans l'écran
|
||||
var cibles = document.querySelectorAll(
|
||||
'.project-card, .quick-link-card, .skill-category, .contact-card, ' +
|
||||
'.login-card, .project-form-card, .cgu-card, .logo-admin-item, ' +
|
||||
'.page-header, .quick-links-header, .skills-header, .projects-title, .logo-banner-header, ' +
|
||||
'.stat-item'
|
||||
);
|
||||
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
|
||||
|
||||
cibles.forEach(function (el, i) {
|
||||
el.classList.add('reveal');
|
||||
// Petit décalage en cascade pour les éléments d'une même grille
|
||||
el.style.setProperty('--reveal-delay', ((i % 4) * 0.08) + 's');
|
||||
observer.observe(el);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+100
-22
@@ -1,37 +1,102 @@
|
||||
<?php
|
||||
<?php
|
||||
|
||||
// Ce Header sera inseré sur des pages grâce a la fonction require_once('./../elements/header.php')
|
||||
// Ce Header sera inseré sur des pages grâce a la fonction require_once('./../elements/header.php')
|
||||
// Rappel des différentes pages : Acceuil, Projets, Contact
|
||||
// Rappel des boutons : Connexion, (Si le temps) Panneau Administrateur (Seulement si l'utilisateur est admin)
|
||||
|
||||
|
||||
// Ajout ici d'un démarrage de session : header étant dans toutes les pages
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
|
||||
// Ajout ici la vérification du captcha
|
||||
require_once('../scripts/requireCaptcha.php');
|
||||
|
||||
// Ajout des logs sur toutes les pages
|
||||
require_once('../functions/sendLogs.php');
|
||||
?>
|
||||
|
||||
<!-- Ajout du CSS Primaire (fait par IA) -->
|
||||
<link rel="stylesheet" href="../styles/primary_css.css">
|
||||
<!-- Ajout de Font Awesome pour les icones de home.php-->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||
// Log de la visite de la page sur Discord (anti-spam : 1 log max par page / minute / visiteur)
|
||||
sendPageLog();
|
||||
|
||||
// Titre de la page : chaque page peut définir $pageTitle avant d'inclure le header
|
||||
$pageTitle = isset($pageTitle) ? $pageTitle . ' — Esteban MOUSSU' : 'Esteban MOUSSU — Portfolio';
|
||||
|
||||
// Description : chaque page peut définir $pageDescription avant d'inclure le header
|
||||
// (sinon description générique, utilisée pour le SEO et les aperçus de partage)
|
||||
$pageDescription = isset($pageDescription)
|
||||
? $pageDescription
|
||||
: 'Portfolio d\'Esteban MOUSSU, étudiant en informatique passionné par les systèmes, les réseaux et le développement web. Projets, expériences et compétences.';
|
||||
|
||||
// URL absolue de la page courante (pour og:url et le lien canonique)
|
||||
$pageUrl = 'https://estebanadmin.fr' . strtok($_SERVER['REQUEST_URI'], '?');
|
||||
|
||||
// Page courante pour souligner le lien actif dans la navigation
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- Meta viewport : indispensable pour que le site soit responsive sur mobile -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo htmlspecialchars($pageTitle); ?></title>
|
||||
|
||||
<!-- SEO : description + lien canonique -->
|
||||
<meta name="description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||
<link rel="canonical" href="<?php echo htmlspecialchars($pageUrl); ?>">
|
||||
|
||||
<!-- Aperçu de partage (réseaux sociaux, Discord, etc.) -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="<?php echo htmlspecialchars($pageTitle); ?>">
|
||||
<meta property="og:description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||
<meta property="og:url" content="<?php echo htmlspecialchars($pageUrl); ?>">
|
||||
<meta property="og:image" content="https://estebanadmin.fr/memoji.png">
|
||||
<meta property="og:locale" content="fr_FR">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="<?php echo htmlspecialchars($pageTitle); ?>">
|
||||
<meta name="twitter:description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||
<meta name="twitter:image" content="https://estebanadmin.fr/memoji.png">
|
||||
|
||||
<!-- Favicon du site -->
|
||||
<link rel="icon" type="image/png" href="../memoji.png">
|
||||
|
||||
<!-- Applique le thème sauvegardé AVANT le chargement du CSS pour éviter un flash sombre/clair -->
|
||||
<script>
|
||||
(function () {
|
||||
if (localStorage.getItem('theme') === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Police moderne Inter (Google Fonts) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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">
|
||||
|
||||
<!-- Ajout du CSS Primaire (fait par IA) -->
|
||||
<link rel="stylesheet" href="../styles/primary_css.css">
|
||||
<!-- Ajout de Font Awesome pour les icones de home.php-->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||
<!-- Devicon : équivalent gratuit de Font Awesome dédié aux logos d'outils/langages/frameworks,
|
||||
utilisé pour le bandeau "Outils que je sais manipuler" de la page d'accueil -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/devicon@2.15.1/devicon.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Header + nav -->
|
||||
<header>
|
||||
<header class="site-header">
|
||||
<!-- Texte qui sera dans le header -->
|
||||
<a href="./../index.php" class="titre-de-page">Esteban MOUSSU - PortFolio</a>
|
||||
|
||||
<div class="header-right">
|
||||
|
||||
<!-- Module de navigation -->
|
||||
<nav>
|
||||
<a href="./../index.php">Accueil</a>
|
||||
<a href="./../pages/projects.php">Projets</a>
|
||||
<a href="./../pages/xp.php">Expériences</a>
|
||||
<a href="./../pages/skills.php">Compétences</a>
|
||||
<a href="./../pages/contact.php">Contact</a>
|
||||
<nav class="site-nav">
|
||||
<a href="./../pages/home.php" class="<?php echo ($currentPage === 'home.php') ? 'active' : ''; ?>">Accueil</a>
|
||||
<a href="./../pages/projects.php" class="<?php echo ($currentPage === 'projects.php') ? 'active' : ''; ?>">Projets</a>
|
||||
<a href="./../pages/xp.php" class="<?php echo ($currentPage === 'xp.php') ? 'active' : ''; ?>">Expériences</a>
|
||||
<a href="./../pages/skills.php" class="<?php echo ($currentPage === 'skills.php') ? 'active' : ''; ?>">Compétences</a>
|
||||
<a href="./../pages/contact.php" class="<?php echo ($currentPage === 'contact.php') ? 'active' : ''; ?>">Contact</a>
|
||||
<?php
|
||||
// Vérification que l'utilisateur est admin, s'il est admin, il aura accès au panel admin
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -40,29 +105,42 @@ require_once('../functions/sendLogs.php');
|
||||
?>
|
||||
|
||||
<!-- Lien vers le panneau admin -->
|
||||
<a href="./../pages/admin.php">Administration</a>
|
||||
|
||||
<a href="./../pages/admin.php" class="<?php echo ($currentPage === 'admin.php') ? 'active' : ''; ?>">Administration</a>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Vérification si l'utilisateur est connecté, si c'est le cas au lieu de Connexion il aura Deconnexion
|
||||
|
||||
|
||||
if (isset($_SESSION['user_id'])){
|
||||
?>
|
||||
<a href="../scripts/disconnect.php">Deconnexion</a>
|
||||
<a href="../scripts/disconnect.php" class="nav-cta">Deconnexion</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<a href="./../pages/login.php">Connexion</a>
|
||||
<a href="./../pages/login.php" class="nav-cta <?php echo ($currentPage === 'login.php') ? 'active' : ''; ?>">Connexion</a>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Fonctionnalité future changement de thème dark / light et panneau admin -->
|
||||
</nav>
|
||||
|
||||
<!-- Bouton de changement de thème dark / light : le choix est mémorisé dans le navigateur (localStorage) -->
|
||||
<button class="theme-toggle" aria-label="Changer de thème"
|
||||
onclick="var h=document.documentElement;var l=h.getAttribute('data-theme')==='light';if(l){h.removeAttribute('data-theme');localStorage.setItem('theme','dark');}else{h.setAttribute('data-theme','light');localStorage.setItem('theme','light');}">
|
||||
<i class="fa-solid fa-sun"></i><i class="fa-solid fa-moon"></i>
|
||||
</button>
|
||||
|
||||
<!-- Bouton burger : visible uniquement sur mobile, ouvre/ferme la navigation -->
|
||||
<button class="nav-toggle" aria-label="Ouvrir le menu" aria-expanded="false"
|
||||
onclick="var h=document.querySelector('.site-header');h.classList.toggle('nav-open');this.setAttribute('aria-expanded',h.classList.contains('nav-open'));">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<main>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Fonction qui va faire une requête SQL afin de collecter les logos
|
||||
// $category vaut 'tool', 'partner', ou null pour récupérer les deux catégories
|
||||
// (toutes catégories confondues, utilisé par le panel admin)
|
||||
// Retourne un tableau vide si la table n'existe pas encore (pas de die() pour que la page
|
||||
// d'accueil continue de fonctionner tant que le script SQL n'a pas été exécuté)
|
||||
function listeLogos($category = null) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
// SELECT * : la colonne icon est optionnelle (le bandeau continue de fonctionner
|
||||
// avec les logos déjà en place même si la migration SQL n'a pas encore été exécutée)
|
||||
if ($category !== null) {
|
||||
$stmt = $dbh->prepare('SELECT * FROM logo_banners WHERE category = :category ORDER BY position ASC, id ASC');
|
||||
$stmt->bindValue(':category', $category, PDO::PARAM_STR);
|
||||
} else {
|
||||
$stmt = $dbh->prepare('SELECT * FROM logo_banners ORDER BY category ASC, position ASC, id ASC');
|
||||
}
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction qui permet de récupérer un logo par id (pour la modification dans le panel Admin)
|
||||
function recupererLogoParId($id) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('SELECT * FROM logo_banners WHERE id = :id');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction interne qui affiche le contenu visuel d'une tuile : une icône Devicon si elle est
|
||||
// renseignée (prioritaire), sinon l'image. Ne retourne rien si aucun des deux n'est présent.
|
||||
function afficherLogoVisuel($logo) {
|
||||
$icone = trim($logo['icon'] ?? '');
|
||||
$image = trim($logo['logo_url'] ?? '');
|
||||
$nom = htmlspecialchars($logo['name']);
|
||||
|
||||
if ($icone !== '') {
|
||||
echo '<i class="' . htmlspecialchars($icone) . '"></i>';
|
||||
} elseif ($image !== '') {
|
||||
echo '<img src="' . htmlspecialchars($image) . '" alt="' . $nom . '" loading="lazy">';
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction qui affiche le bandeau défilant d'une catégorie de logos sur la page publique
|
||||
// N'affiche rien si la catégorie est vide (la section entière est masquée côté page d'accueil)
|
||||
function afficherLogos($category) {
|
||||
$logos = listeLogos($category);
|
||||
|
||||
if (empty($logos)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sens = $category === 'partner' ? ' logo-marquee-reverse' : '';
|
||||
|
||||
echo '<div class="logo-marquee' . $sens . '">';
|
||||
echo '<div class="logo-track">';
|
||||
|
||||
// On répète deux fois la liste : le défilement CSS boucle sur -50%, ce qui donne
|
||||
// un effet de scroll infini fluide et sans coupure (voir styles/primary_css.css)
|
||||
for ($repetition = 0; $repetition < 2; $repetition++) {
|
||||
foreach ($logos as $logo) {
|
||||
$nom = htmlspecialchars($logo['name']);
|
||||
$lien = trim($logo['link_url'] ?? '');
|
||||
|
||||
if ($lien !== '') {
|
||||
echo '<a class="logo-tile" href="' . htmlspecialchars($lien) . '" target="_blank" rel="noopener noreferrer" title="' . $nom . '">';
|
||||
} else {
|
||||
echo '<div class="logo-tile" title="' . $nom . '">';
|
||||
}
|
||||
|
||||
afficherLogoVisuel($logo);
|
||||
echo $lien !== '' ? '</a>' : '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
// Fonction qui affiche la liste des logos (toutes catégories) avec boutons de
|
||||
// modification / suppression, pour le panel admin
|
||||
function afficherLogosAdmin() {
|
||||
$logos = listeLogos();
|
||||
|
||||
if (empty($logos)) {
|
||||
echo '<p class="admin-notice">Aucun logo pour le moment.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="logo-admin-grid">';
|
||||
|
||||
foreach ($logos as $logo) {
|
||||
$id = (int) $logo['id'];
|
||||
$nom = htmlspecialchars($logo['name']);
|
||||
$badge = $logo['category'] === 'partner' ? 'Entreprise' : 'Outil';
|
||||
|
||||
echo '<div class="logo-admin-item">';
|
||||
echo '<div class="logo-admin-preview">';
|
||||
afficherLogoVisuel($logo);
|
||||
echo '</div>';
|
||||
echo '<div class="logo-admin-info">';
|
||||
echo '<span class="tag">' . $badge . '</span>';
|
||||
echo '<p>' . $nom . '</p>';
|
||||
echo '</div>';
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_logo=' . $id . '#admin-logos">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeLogo.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce logo ?\');">Supprimer</a>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
@@ -10,13 +10,22 @@ function listeProjets() {
|
||||
|
||||
// Essayer
|
||||
try {
|
||||
// Requête SQL qui récupère titre, tags, description, image_url
|
||||
$stmt = $dbh->prepare('SELECT id, title, status, tags, description, image_url FROM projects ORDER BY id DESC');
|
||||
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||
// stocké à part dans project_pdfs (voir migration Juillet 2026 (6)), on ne récupère
|
||||
// ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
|
||||
// (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste de projets
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT p.id, p.title, p.status, p.tags, p.description, p.image_url, p.markdown, p.link_url, p.created_at,
|
||||
pp.pdf_filename
|
||||
FROM projects p
|
||||
LEFT JOIN project_pdfs pp ON pp.project_id = p.id
|
||||
ORDER BY p.id DESC
|
||||
');
|
||||
// Execution de la requête SQL
|
||||
$stmt->execute();
|
||||
// Retourne toutes les données collectés dans un ARRAY
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) { // En cas d'erreur
|
||||
} catch (PDOException $e) { // En cas d'erreur
|
||||
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
||||
return [];
|
||||
}
|
||||
@@ -27,7 +36,15 @@ function recupererProjetParId($id) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('SELECT id, title, status, tags, description, image_url FROM projects WHERE id = :id');
|
||||
// Même principe que listeProjets() : le PDF est joint pour son nom de fichier
|
||||
// uniquement, jamais son contenu binaire
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT p.id, p.title, p.status, p.tags, p.description, p.image_url, p.markdown, p.link_url, p.created_at,
|
||||
pp.pdf_filename
|
||||
FROM projects p
|
||||
LEFT JOIN project_pdfs pp ON pp.project_id = p.id
|
||||
WHERE p.id = :id
|
||||
');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
@@ -50,10 +67,14 @@ function afficherProjets() {
|
||||
// Pour chaque projet dans le ARRAY
|
||||
foreach ($projets as $projet) {
|
||||
// On récupère le titre, la description et l'image et le tableau avec les tags
|
||||
$id = (int) $projet['id'];
|
||||
$titre = htmlspecialchars($projet['title']);
|
||||
$description = nl2br(htmlspecialchars($projet['description']));
|
||||
$image = htmlspecialchars($projet['image_url']);
|
||||
$status = htmlspecialchars($projet['status'] ?? '');
|
||||
$markdown = trim($projet['markdown'] ?? '');
|
||||
$lien = trim($projet['link_url'] ?? '');
|
||||
$pdf = trim($projet['pdf_filename'] ?? '');
|
||||
$tags = [];
|
||||
|
||||
// S'il y a des tags alors on les sépares grâce à la virgule que l'user à ajouté dans le champs (tags1,tags2,etcc...)
|
||||
@@ -88,8 +109,21 @@ function afficherProjets() {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
// Ajout de la description et fin des classes
|
||||
// Ajout de la description et fin des classes
|
||||
echo '<p>' . $description . '</p>';
|
||||
|
||||
// Indicateur de page détaillée : markdown > PDF > lien externe > rien
|
||||
// Le lien rend toute la carte cliquable (voir CSS .card-link)
|
||||
if ($markdown !== '') {
|
||||
echo '<a class="card-link" href="./details.php?type=projet&id=' . $id . '"><i class="fa-solid fa-file-lines"></i> Voir la page détaillée</a>';
|
||||
} elseif ($pdf !== '') {
|
||||
echo '<a class="card-link" href="./details.php?type=projet&id=' . $id . '"><i class="fa-solid fa-file-pdf"></i> Voir le PDF</a>';
|
||||
} elseif ($lien !== '') {
|
||||
echo '<a class="card-link" href="' . $lien . '" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-arrow-up-right-from-square"></i> Voir le lien externe</a>';
|
||||
} else {
|
||||
echo '<span class="card-link card-link-none"><i class="fa-solid fa-eye-slash"></i> Pas de page détaillée</span>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
echo '</article>';
|
||||
@@ -144,8 +178,8 @@ function afficherProjetsAdmin() {
|
||||
|
||||
// Boutons qui vont permettrent la modification / Suppresion
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeProject.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce projet ?\');">Supprimer</a>';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '#admin-projets">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeProject.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce projet ?\');">Supprimer</a>';
|
||||
echo '</div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Fonction qui va faire une requête SQL afin de collecter toutes les écoles / formations
|
||||
// Retourne un tableau vide si la table n'existe pas encore (pas de die() pour que la page
|
||||
// Expériences continue de fonctionner tant que le script SQL n'a pas été exécuté)
|
||||
function listeEcoles() {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||
// stocké à part dans school_pdfs (voir migration Juillet 2026 (6)), on ne récupère
|
||||
// ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
|
||||
// (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste d'écoles
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT s.id, s.title, s.status, s.tags, s.description, s.duration, s.image_url, s.markdown, s.link_url, s.created_at,
|
||||
sp.pdf_filename
|
||||
FROM schools s
|
||||
LEFT JOIN school_pdfs sp ON sp.school_id = s.id
|
||||
ORDER BY s.id DESC
|
||||
');
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction qui permet de récupérer une école par id (pour la modification dans le panel Admin)
|
||||
function recupererEcoleParId($id) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
// Même principe que listeEcoles() : le PDF est joint pour son nom de fichier
|
||||
// uniquement, jamais son contenu binaire
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT s.id, s.title, s.status, s.tags, s.description, s.duration, s.image_url, s.markdown, s.link_url, s.created_at,
|
||||
sp.pdf_filename
|
||||
FROM schools s
|
||||
LEFT JOIN school_pdfs sp ON sp.school_id = s.id
|
||||
WHERE s.id = :id
|
||||
');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction interne qui affiche une carte école (utilisée par la page publique et le panel admin)
|
||||
function afficherEcoleCard($ecole, $admin = false) {
|
||||
$id = (int) $ecole['id'];
|
||||
$titre = htmlspecialchars($ecole['title']);
|
||||
$description = nl2br(htmlspecialchars($ecole['description']));
|
||||
$duration = htmlspecialchars($ecole['duration']);
|
||||
$image = htmlspecialchars($ecole['image_url']);
|
||||
$status = htmlspecialchars($ecole['status'] ?? '');
|
||||
$markdown = trim($ecole['markdown'] ?? '');
|
||||
$lien = trim($ecole['link_url'] ?? '');
|
||||
$pdf = trim($ecole['pdf_filename'] ?? '');
|
||||
$tags = [];
|
||||
|
||||
// S'il y a des tags alors on les sépares grâce à la virgule (tags1,tags2,etc...)
|
||||
if (!empty($ecole['tags'])) {
|
||||
$tags = array_filter(array_map('trim', explode(',', $ecole['tags'])));
|
||||
}
|
||||
|
||||
echo '<article class="project-card">';
|
||||
|
||||
// Si la donnée avec l'image n'est pas null alors on l'affiche
|
||||
if (!empty($image)) {
|
||||
echo '<div class="project-banner">';
|
||||
echo '<img src="' . $image . '" alt="Image de l ecole ' . $titre . '">';
|
||||
|
||||
if (!empty($status)) {
|
||||
echo '<div class="project-state">' . $status . '</div>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
echo '<div class="project-content">';
|
||||
echo '<h2>' . $titre . '</h2>';
|
||||
|
||||
echo '<p><strong>Période :</strong> ' . $duration . '</p>';
|
||||
|
||||
if (!empty($tags)) {
|
||||
echo '<div class="project-tags">';
|
||||
foreach ($tags as $tag) {
|
||||
echo '<span class="tag">' . htmlspecialchars($tag) . '</span>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
echo '<p>' . $description . '</p>';
|
||||
|
||||
// Indicateur de page détaillée (uniquement sur la page publique) : markdown > PDF > lien externe > rien
|
||||
if (!$admin) {
|
||||
if ($markdown !== '') {
|
||||
echo '<a class="card-link" href="./details.php?type=ecole&id=' . $id . '"><i class="fa-solid fa-file-lines"></i> Voir la page détaillée</a>';
|
||||
} elseif ($pdf !== '') {
|
||||
echo '<a class="card-link" href="./details.php?type=ecole&id=' . $id . '"><i class="fa-solid fa-file-pdf"></i> Voir le PDF</a>';
|
||||
} elseif ($lien !== '') {
|
||||
echo '<a class="card-link" href="' . $lien . '" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-arrow-up-right-from-square"></i> Voir le lien externe</a>';
|
||||
} else {
|
||||
echo '<span class="card-link card-link-none"><i class="fa-solid fa-eye-slash"></i> Pas de page détaillée</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Boutons de modification / suppression pour le panel admin
|
||||
if ($admin) {
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_school=' . $id . '#admin-ecoles">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeSchool.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette école ?\');">Supprimer</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
echo '</article>';
|
||||
}
|
||||
|
||||
// Fonction qui va afficher les écoles sur la page publique
|
||||
function afficherEcoles() {
|
||||
$ecoles = listeEcoles();
|
||||
|
||||
// Si la table est vide ou n'existe pas encore on n'affiche rien
|
||||
if (empty($ecoles)) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="projects-list">';
|
||||
foreach ($ecoles as $ecole) {
|
||||
afficherEcoleCard($ecole, false);
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
// Fonction identique à celle en haut mais ajout de boutons pour le panel admin
|
||||
function afficherEcolesAdmin() {
|
||||
$ecoles = listeEcoles();
|
||||
|
||||
echo '<div class="projects-list">';
|
||||
foreach ($ecoles as $ecole) {
|
||||
afficherEcoleCard($ecole, true);
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Fonction qui récupère un réglage dans la table settings (clé / valeur)
|
||||
// Retourne null si le réglage n'existe pas OU si la table n'a pas encore été créée :
|
||||
// contrairement aux autres fonctions on ne fait pas de die() ici pour que le site
|
||||
// continue de fonctionner tant que le script SQL n'a pas été exécuté
|
||||
function recupererSetting($name) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('SELECT value FROM settings WHERE name = :name');
|
||||
$stmt->bindValue(':name', $name, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $row ? $row['value'] : null;
|
||||
} catch (PDOException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction qui affiche la notification de la page d'accueil (si elle est activée)
|
||||
function afficherNotice() {
|
||||
$enabled = recupererSetting('site_notice_enabled');
|
||||
$titre = recupererSetting('site_notice_title');
|
||||
$texte = recupererSetting('site_notice_text');
|
||||
|
||||
// Valeurs par défaut tant que la table settings n'existe pas encore
|
||||
if ($titre === null && $texte === null) {
|
||||
$enabled = '1';
|
||||
$titre = 'Des bugs peuvent encore se produire.';
|
||||
$texte = "Si le CSS ou les images s'affichent mal, pensez à vider le cache de votre navigateur (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications.";
|
||||
}
|
||||
|
||||
// Si la notification est désactivée ou vide on n'affiche rien
|
||||
if ($enabled !== '1' || (empty($titre) && empty($texte))) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<p class="dev-cache-notice">';
|
||||
if (!empty($titre)) {
|
||||
echo '<strong>' . htmlspecialchars($titre) . '</strong> ';
|
||||
}
|
||||
echo nl2br(htmlspecialchars($texte));
|
||||
echo '</p>';
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Fonction qui va faire une requête SQL afin de collecter toutes les catégories de compétences
|
||||
// Retourne un tableau vide si la table n'existe pas encore (pas de die() pour que la page
|
||||
// Compétences continue de fonctionner tant que le script SQL n'a pas été exécuté)
|
||||
function listeSkills() {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('SELECT id, title, icon, items, position FROM skills ORDER BY position ASC, id ASC');
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction qui permet de récupérer une catégorie de compétences par id (pour la modification dans le panel Admin)
|
||||
function recupererSkillParId($id) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('SELECT id, title, icon, items, position FROM skills WHERE id = :id');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Icônes Devicon dont la couleur de marque "colored" est noire ou quasi-noire (vérifié dans
|
||||
// devicon.min.css : linux #000, apple #000, bash #293138, ansible #1a1918) : invisibles sur
|
||||
// la tuile sombre du bandeau. On les affiche dans la couleur claire par défaut à la place.
|
||||
const DEVICON_COLORED_TROP_SOMBRE = ['devicon-linux-plain', 'devicon-apple-plain', 'devicon-bash-plain', 'devicon-ansible-plain'];
|
||||
|
||||
// Fonction interne qui affiche le visuel (icône ou image) d'un item de compétence.
|
||||
// Format accepté avant le "|" : soit des classes d'icône (Font Awesome ou Devicon), soit
|
||||
// "img:URL" pour utiliser une vraie image de logo (pour les outils absents des polices
|
||||
// d'icônes, ex : GLPI, FOG, Proxmox, VirtualBox, Guacamole).
|
||||
// $colorise active les vraies couleurs de marque des icônes Devicon (utilisé uniquement
|
||||
// pour le bandeau de la home : sur la page Compétences les icônes restent en orange
|
||||
// uniforme, cohérent avec le reste de la liste)
|
||||
function afficherVisuelItem($itemIcone, $classe = '', $colorise = false) {
|
||||
if (strpos($itemIcone, 'img:') === 0) {
|
||||
$image = htmlspecialchars(substr($itemIcone, 4));
|
||||
echo '<img class="' . $classe . '" src="' . $image . '" alt="" loading="lazy">';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($colorise && strpos($itemIcone, 'devicon-') === 0 && strpos($itemIcone, 'colored') === false) {
|
||||
$estTropSombre = false;
|
||||
foreach (DEVICON_COLORED_TROP_SOMBRE as $slug) {
|
||||
if (strpos($itemIcone, $slug) === 0) {
|
||||
$estTropSombre = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$estTropSombre) {
|
||||
$itemIcone .= ' colored';
|
||||
}
|
||||
}
|
||||
|
||||
echo '<i class="' . $classe . ' ' . htmlspecialchars($itemIcone) . '"></i>';
|
||||
}
|
||||
|
||||
// Fonction interne qui affiche une carte de compétences (utilisée par la page publique et le panel admin)
|
||||
// Les items sont stockés un par ligne, au format "classes-icone|Texte" (icône optionnelle)
|
||||
function afficherSkillCard($skill, $admin = false) {
|
||||
$id = (int) $skill['id'];
|
||||
$titre = htmlspecialchars($skill['title']);
|
||||
$icone = htmlspecialchars(!empty($skill['icon']) ? $skill['icon'] : 'fa-solid fa-star');
|
||||
|
||||
echo '<div class="skill-category">';
|
||||
echo '<div class="skill-category-icon"><i class="' . $icone . '"></i></div>';
|
||||
echo '<h2>' . $titre . '</h2>';
|
||||
echo '<ul class="skill-list">';
|
||||
|
||||
// Un item par ligne
|
||||
$items = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $skill['items'])));
|
||||
foreach ($items as $item) {
|
||||
// Icône optionnelle avant le | sinon icône par défaut
|
||||
$itemIcone = 'fa-solid fa-check';
|
||||
$label = $item;
|
||||
if (strpos($item, '|') !== false) {
|
||||
list($itemIcone, $label) = array_map('trim', explode('|', $item, 2));
|
||||
}
|
||||
echo '<li>';
|
||||
afficherVisuelItem($itemIcone, 'skill-item-visual');
|
||||
echo ' ' . htmlspecialchars($label) . '</li>';
|
||||
}
|
||||
|
||||
echo '</ul>';
|
||||
|
||||
// Boutons de modification / suppression pour le panel admin
|
||||
if ($admin) {
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_skill=' . $id . '#admin-competences">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeSkill.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette catégorie de compétences ?\');">Supprimer</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
// Fonction qui va afficher les compétences sur la page publique
|
||||
function afficherSkills() {
|
||||
$skills = listeSkills();
|
||||
|
||||
// Si la table est vide ou n'existe pas encore
|
||||
if (empty($skills)) {
|
||||
echo '<p class="admin-notice">Les compétences seront bientôt disponibles.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="skills-grid">';
|
||||
foreach ($skills as $skill) {
|
||||
afficherSkillCard($skill, false);
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
// Fonction identique à celle en haut mais ajout de boutons pour le panel admin
|
||||
function afficherSkillsAdmin() {
|
||||
$skills = listeSkills();
|
||||
|
||||
echo '<div class="skills-grid">';
|
||||
foreach ($skills as $skill) {
|
||||
afficherSkillCard($skill, true);
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
// Items exclus du bandeau de la home au cas par cas, en plus du filtre automatique
|
||||
// (icône Devicon "réelle" mais jugée pas assez reconnaissable seule, sans le texte à côté)
|
||||
const OUTILS_EXCLUS_DU_BANDEAU = ['Bash'];
|
||||
|
||||
// Fonction qui récupère tous les items de compétences représentant un vrai outil identifiable
|
||||
// (icône Devicon ou image "img:"), toutes catégories confondues. Les items en texte seul
|
||||
// SONT EXCLUS, ainsi que les icônes Font Awesome génériques (fa-solid/fa-brands) : dans un
|
||||
// bandeau qui n'affiche pas le texte à côté (juste une info-bulle au survol), une icône
|
||||
// générique comme un pictogramme "carte" pour "Adressage IP" ou "carnet de contact" pour
|
||||
// "LDAP" ne représente aucun outil reconnaissable — ça reste illisible pour un visiteur qui
|
||||
// ne survole pas. Ces items restent affichés normalement sur la page Compétences (avec leur
|
||||
// texte à côté, donc sans ambiguïté), seul le bandeau de la home les filtre.
|
||||
function listeIconesCompetences() {
|
||||
$categories = listeSkills();
|
||||
$resultats = [];
|
||||
|
||||
foreach ($categories as $categorie) {
|
||||
$items = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $categorie['items'])));
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (strpos($item, '|') === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
list($icone, $label) = array_map('trim', explode('|', $item, 2));
|
||||
if ($icone === '' || $label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seuls les vrais logos (Devicon ou image) sont assez reconnaissables sans texte
|
||||
$estUnVraiLogo = strpos($icone, 'devicon-') === 0 || strpos($icone, 'img:') === 0;
|
||||
if (!$estUnVraiLogo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($label, OUTILS_EXCLUS_DU_BANDEAU, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resultats[] = ['icon' => $icone, 'label' => $label];
|
||||
}
|
||||
}
|
||||
|
||||
return $resultats;
|
||||
}
|
||||
|
||||
// Fonction qui affiche le bandeau défilant des outils, directement à partir des icônes
|
||||
// de la page Compétences (voir listeIconesCompetences). Réutilise les mêmes classes CSS
|
||||
// que les bandeaux de logo_banners pour un rendu visuel identique (voir primary_css.css).
|
||||
// "logo-marquee-tools" désactive le filtre grayscale/survol : contrairement aux logos
|
||||
// d'entreprises, on veut que les vrais outils soient reconnaissables immédiatement.
|
||||
function afficherOutilsCompetences() {
|
||||
$outils = listeIconesCompetences();
|
||||
|
||||
if (empty($outils)) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="logo-marquee logo-marquee-tools">';
|
||||
echo '<div class="logo-track">';
|
||||
|
||||
// On répète deux fois la liste : le défilement CSS boucle sur -50%, ce qui donne
|
||||
// un effet de scroll infini fluide et sans coupure
|
||||
for ($repetition = 0; $repetition < 2; $repetition++) {
|
||||
foreach ($outils as $outil) {
|
||||
$label = htmlspecialchars($outil['label']);
|
||||
|
||||
echo '<div class="logo-tile" title="' . $label . '">';
|
||||
afficherVisuelItem($outil['icon'], '', true);
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Fonction qui récupère les chiffres clés affichés sur la home (bandeau de stats)
|
||||
// Chaque compteur retombe sur 0 en cas d'erreur (table pas encore créée...) plutôt
|
||||
// que de bloquer la page d'accueil
|
||||
function recupererStatsHome() {
|
||||
global $dbh;
|
||||
|
||||
$stats = ['projets' => 0, 'experiences' => 0];
|
||||
|
||||
try {
|
||||
$stats['projets'] = (int) $dbh->query('SELECT COUNT(*) FROM projects')->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
// Table pas encore créée : on laisse le compteur à 0
|
||||
}
|
||||
|
||||
try {
|
||||
$stats['experiences'] = (int) $dbh->query('SELECT COUNT(*) FROM experiences')->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
// Table pas encore créée : on laisse le compteur à 0
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function afficherUsersAdmin()
|
||||
echo '<p><strong>Créé le :</strong> ' . htmlspecialchars($user["created_at"]) . '</p>';
|
||||
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a href="../scripts/removeUser.php?id=' . $user["id"] . '" class="btn-submit btn-delete" onclick="return confirm(\'Supprimer cet utilisateur ?\')">Supprimer</a>';
|
||||
echo '<a href="../scripts/removeUser.php?id=' . $user["id"] . '&csrf_token=' . urlencode(csrfToken()) . '" class="btn-submit btn-delete" onclick="return confirm(\'Supprimer cet utilisateur ?\')">Supprimer</a>';
|
||||
echo '</div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
+42
-9
@@ -13,13 +13,22 @@ function listeXP() {
|
||||
|
||||
// Essayer
|
||||
try {
|
||||
// Requête SQL qui récupère titre, type, tags, description, durée, image_url
|
||||
$stmt = $dbh->prepare('SELECT id, title, status, xptype, tags, description, duration, image_url FROM experiences ORDER BY id DESC');
|
||||
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||
// stocké à part dans experience_pdfs (voir migration Juillet 2026 (6)), on ne
|
||||
// récupère ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu
|
||||
// binaire (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste d'expériences
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT e.id, e.title, e.status, e.xptype, e.tags, e.description, e.duration, e.image_url, e.markdown, e.link_url, e.created_at,
|
||||
ep.pdf_filename
|
||||
FROM experiences e
|
||||
LEFT JOIN experience_pdfs ep ON ep.experience_id = e.id
|
||||
ORDER BY e.id DESC
|
||||
');
|
||||
// Execution de la requête SQL
|
||||
$stmt->execute();
|
||||
// Retourne toutes les données collectés dans un ARRAY
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) { // En cas d'erreur
|
||||
} catch (PDOException $e) { // En cas d'erreur
|
||||
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
||||
return [];
|
||||
}
|
||||
@@ -30,13 +39,20 @@ function recupererXPParId($id) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
// Préparation de la requête préparée
|
||||
$stmt = $dbh->prepare('SELECT id, title, status, xptype, tags, description, duration, image_url FROM experiences WHERE id = :id');
|
||||
// Même principe que listeXP() : le PDF est joint pour son nom de fichier
|
||||
// uniquement, jamais son contenu binaire
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT e.id, e.title, e.status, e.xptype, e.tags, e.description, e.duration, e.image_url, e.markdown, e.link_url, e.created_at,
|
||||
ep.pdf_filename
|
||||
FROM experiences e
|
||||
LEFT JOIN experience_pdfs ep ON ep.experience_id = e.id
|
||||
WHERE e.id = :id
|
||||
');
|
||||
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
// En cas d'erreur => renvoyer un message avec l'erreur
|
||||
// En cas d'erreur => renvoyer un message avec l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur de connexion à la base de donnée portfolio :" . $e->getMessage());
|
||||
// Retourner false en cas d'eerreur
|
||||
@@ -58,12 +74,16 @@ function afficherXP() {
|
||||
// Pour chaque experience dans le ARRAY
|
||||
foreach ($experiences as $experience) {
|
||||
// On récupère le titre, le type, la description, la durée et l'image et le tableau avec les tags
|
||||
$id = (int) $experience['id'];
|
||||
$titre = htmlspecialchars($experience['title']);
|
||||
$xptype = htmlspecialchars($experience['xptype']);
|
||||
$description = nl2br(htmlspecialchars($experience['description']));
|
||||
$duration = htmlspecialchars($experience['duration']);
|
||||
$image = htmlspecialchars($experience['image_url']);
|
||||
$status = htmlspecialchars($experience['status'] ?? '');
|
||||
$markdown = trim($experience['markdown'] ?? '');
|
||||
$lien = trim($experience['link_url'] ?? '');
|
||||
$pdf = trim($experience['pdf_filename'] ?? '');
|
||||
$tags = [];
|
||||
|
||||
|
||||
@@ -107,8 +127,21 @@ function afficherXP() {
|
||||
}
|
||||
|
||||
|
||||
// Ajout de la description et fin des classes
|
||||
// Ajout de la description et fin des classes
|
||||
echo '<p>' . $description . '</p>';
|
||||
|
||||
// Indicateur de page détaillée : markdown > PDF > lien externe > rien
|
||||
// Le lien rend toute la carte cliquable (voir CSS .card-link)
|
||||
if ($markdown !== '') {
|
||||
echo '<a class="card-link" href="./details.php?type=experience&id=' . $id . '"><i class="fa-solid fa-file-lines"></i> Voir la page détaillée</a>';
|
||||
} elseif ($pdf !== '') {
|
||||
echo '<a class="card-link" href="./details.php?type=experience&id=' . $id . '"><i class="fa-solid fa-file-pdf"></i> Voir le PDF</a>';
|
||||
} elseif ($lien !== '') {
|
||||
echo '<a class="card-link" href="' . $lien . '" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-arrow-up-right-from-square"></i> Voir le lien externe</a>';
|
||||
} else {
|
||||
echo '<span class="card-link card-link-none"><i class="fa-solid fa-eye-slash"></i> Pas de page détaillée</span>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
|
||||
@@ -175,8 +208,8 @@ function afficherXPAdmin() {
|
||||
|
||||
// Boutons qui vont permettrent la modification / Suppresion
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeXP.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette expérience ?\');">Supprimer</a>';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '#admin-experiences">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeXP.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette expérience ?\');">Supprimer</a>';
|
||||
echo '</div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
// Fonction utilitaire de validation/lecture d'un PDF envoyé par formulaire (drag & drop ou clic)
|
||||
// Utilisée par les scripts d'ajout/modification de projets, expériences et écoles
|
||||
|
||||
// Taille max acceptée par l'application : à ajuster avec upload_max_filesize / post_max_size
|
||||
// (php.ini) et max_allowed_packet (MySQL) si besoin d'accepter des PDF plus lourds
|
||||
define('PDF_MAX_SIZE', 15 * 1024 * 1024); // 15 Mo
|
||||
|
||||
// Détecte le cas où le fichier envoyé dépasse post_max_size (php.ini)
|
||||
// Dans ce cas PHP vide $_POST et $_FILES AVANT même d'exécuter le script : sans cette
|
||||
// détection, le formulaire échoue silencieusement (aucun champ, aucune erreur visible)
|
||||
function postDepasseLimite() {
|
||||
$contentLength = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
|
||||
return $_SERVER['REQUEST_METHOD'] === 'POST' && $contentLength > 0 && empty($_POST) && empty($_FILES);
|
||||
}
|
||||
|
||||
// Message d'erreur explicite à afficher quand postDepasseLimite() est vrai
|
||||
function messageDepassementPost() {
|
||||
return "Le fichier envoyé est trop volumineux pour le serveur (limite actuelle : "
|
||||
. ini_get('post_max_size') . "). Réduis la taille du PDF, ou augmente "
|
||||
. "post_max_size et upload_max_filesize côté serveur.";
|
||||
}
|
||||
|
||||
// Vérifie et lit un fichier PDF envoyé en upload ($_FILES)
|
||||
// Retourne :
|
||||
// - null si aucun fichier n'a été envoyé (ce n'est pas une erreur)
|
||||
// - une chaîne de texte si le fichier est invalide (message d'erreur à afficher)
|
||||
// - ['filename' => ..., 'data' => ...] si le PDF est valide
|
||||
function lireUploadPdf($champ = 'pdf') {
|
||||
if (!isset($_FILES[$champ]) || $_FILES[$champ]['error'] === UPLOAD_ERR_NO_FILE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($_FILES[$champ]['error'] !== UPLOAD_ERR_OK) {
|
||||
return "Erreur lors de l'envoi du PDF (code " . $_FILES[$champ]['error'] . ").";
|
||||
}
|
||||
|
||||
if ($_FILES[$champ]['size'] > PDF_MAX_SIZE) {
|
||||
return "Le PDF dépasse la taille maximale autorisée (15 Mo).";
|
||||
}
|
||||
|
||||
// Vérification de l'extension
|
||||
$extension = strtolower(pathinfo($_FILES[$champ]['name'], PATHINFO_EXTENSION));
|
||||
if ($extension !== 'pdf') {
|
||||
return "Le fichier doit être un PDF (extension .pdf).";
|
||||
}
|
||||
|
||||
// Vérification du type réel du fichier (pas seulement l'extension, pour éviter
|
||||
// qu'un fichier malveillant renommé en .pdf soit accepté)
|
||||
// (pas de finfo_close : déprécié depuis PHP 8.5, la ressource se libère toute seule)
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $_FILES[$champ]['tmp_name']);
|
||||
|
||||
if ($mime !== 'application/pdf') {
|
||||
return "Le fichier envoyé n'est pas un PDF valide.";
|
||||
}
|
||||
|
||||
$contenu = file_get_contents($_FILES[$champ]['tmp_name']);
|
||||
if ($contenu === false) {
|
||||
return "Impossible de lire le fichier envoyé.";
|
||||
}
|
||||
|
||||
return [
|
||||
'filename' => basename($_FILES[$champ]['name']),
|
||||
'data' => $contenu
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// Sécurité transverse : cookie de session durci, en-têtes anti-clickjacking et
|
||||
// protection CSRF. A inclure à la place d'un simple session_start() sur toute
|
||||
// page ou script qui utilise une session (remplace l'appel direct partout).
|
||||
|
||||
// Règle les paramètres du cookie de session AVANT de démarrer la session (ils
|
||||
// n'ont aucun effet une fois la session déjà démarrée) :
|
||||
// - httponly : empêche le JavaScript de lire le cookie (limite le vol de session via une XSS)
|
||||
// - samesite=Lax : bloque une partie des requêtes intersites (CSRF)
|
||||
// - secure : n'est activé que si la connexion est déjà en HTTPS (sinon le cookie ne partirait jamais en HTTP)
|
||||
function configureSecureSessionCookies() {
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||
]);
|
||||
}
|
||||
|
||||
// Anti-clickjacking : interdit d'afficher le site dans une <iframe> d'un autre site
|
||||
header('X-Frame-Options: SAMEORIGIN');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
configureSecureSessionCookies();
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Génère (si besoin) et retourne le jeton CSRF de la session en cours, à
|
||||
// placer dans un champ caché de chaque formulaire admin ou dans les liens
|
||||
// GET qui déclenchent une action (suppression, export...)
|
||||
function csrfToken() {
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
// Vérifie le jeton CSRF reçu (POST ou GET) ; arrête le script si invalide.
|
||||
// A appeler juste après la vérification du rôle admin dans chaque script d'action.
|
||||
function requireCsrfToken() {
|
||||
$recu = $_POST['csrf_token'] ?? $_GET['csrf_token'] ?? '';
|
||||
if (empty($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $recu)) {
|
||||
http_response_code(403);
|
||||
die("Requête refusée : jeton de sécurité invalide ou expiré. Recharge la page et réessaie.");
|
||||
}
|
||||
}
|
||||
+49
-4
@@ -1,21 +1,36 @@
|
||||
<?php
|
||||
// On cache les webhooks discord => Correction de faille de sécurité
|
||||
require_once('../../idDiscord.php');
|
||||
// __DIR__ = chemin absolu du dossier functions : le require fonctionne ainsi quel que soit
|
||||
// le dossier depuis lequel le script est appelé (pages/, scripts/, racine...)
|
||||
require_once(__DIR__ . '/../../idDiscord.php');
|
||||
// EXEMPLE de a quoi ressemble idDiscord :
|
||||
// define('ID_DISCORD1', 'https://discord.com/api/webhooks/URL_WEBHOOK1');
|
||||
// define('ID_DISCORD2', 'https://discord.com/api/webhooks/URL_WEBHOOK2');
|
||||
|
||||
|
||||
// Fonction qui va ENVOYER des logs sur le webhook discord
|
||||
function sendLog($action, $user="") {
|
||||
function sendLog($action, $user = "") {
|
||||
// Si le webhook n'est pas configuré on ne bloque pas le site
|
||||
if (!defined('ID_DISCORD1') || ID_DISCORD1 == '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$webhook = ID_DISCORD1; // URL du webhook Discord
|
||||
|
||||
// Si l'utilisateur n'est pas précisé on prend celui de la session (connecté)
|
||||
if ($user == "" && isset($_SESSION['username'])) {
|
||||
$user = $_SESSION['username'];
|
||||
}
|
||||
|
||||
$contenuLog = "**Nouvelle log du portfolio**\n\n";
|
||||
$contenuLog .= "**Action :** " . $action . "\n";
|
||||
if ($user != "") {
|
||||
$contenuLog .= "**Utilisateur :** " . $user . "\n";
|
||||
} else {
|
||||
$contenuLog .= "**Utilisateur :** non défini". "\n";
|
||||
$contenuLog .= "**Utilisateur :** non défini" . "\n";
|
||||
}
|
||||
$contenuLog .= "**IP :** " . ($_SERVER['REMOTE_ADDR'] ?? 'inconnue') . "\n";
|
||||
$contenuLog .= "**Date :** " . date('d/m/Y H:i:s');
|
||||
|
||||
$data = [
|
||||
"content" => $contenuLog
|
||||
@@ -28,6 +43,36 @@ function sendLog($action, $user="") {
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataJson);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// Timeouts courts : si Discord est lent ou down, le site ne doit pas être ralenti
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 800);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1500);
|
||||
|
||||
// CORRECTION DU BUG : curl_exec() n'était jamais appelé, la requête n'était
|
||||
// donc jamais envoyée à Discord. C'est pour ça que les logs n'ont jamais marché.
|
||||
// (pas de curl_close : déprécié depuis PHP 8.5, la ressource se libère toute seule)
|
||||
curl_exec($ch);
|
||||
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
// Discord renvoie 204 quand le message est bien envoyé
|
||||
return $codeHttp == 204;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// Log de visite de page : appelée par le header sur toutes les pages
|
||||
// Anti-spam : Discord limite à ~30 messages/minute par webhook, donc une même page
|
||||
// n'est loguée qu'une fois par minute et par visiteur (stocké dans sa session)
|
||||
function sendPageLog() {
|
||||
$page = basename($_SERVER['PHP_SELF'] ?? 'inconnue');
|
||||
|
||||
if (!isset($_SESSION['pages_log'])) {
|
||||
$_SESSION['pages_log'] = [];
|
||||
}
|
||||
|
||||
$maintenant = time();
|
||||
if (isset($_SESSION['pages_log'][$page]) && ($maintenant - $_SESSION['pages_log'][$page]) < 60) {
|
||||
return; // Page déjà loguée il y a moins d'une minute pour ce visiteur
|
||||
}
|
||||
|
||||
$_SESSION['pages_log'][$page] = $maintenant;
|
||||
sendLog("Visite de la page " . $page);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<?php
|
||||
// Ce projet est entièrement développé par Esteban MOUSSU
|
||||
// 2026 - Projet Hébergé par l'entreprise TerraTech Hebergement en collaboration avec l'association Eldoria Network
|
||||
// Le CSS est **probablement** généré par intelligence artificielle ou récupéré d'autre projet, cependant le code PHP / HTML / JS est globalement fait par un humain
|
||||
|
||||
// Le CSS est généré par intelligence artificielle ou récupéré d'autre projet.
|
||||
// Note 1 : Pour lancer un serveur local PHP Depuis VsCode : 'php -v' pour vérifier que php est bien installé et 'php -S localhost:8000' pour lancer le serveur
|
||||
// Note 2 : Je m'aide désormais de l'autocompletion de vsCode pour accélerer le développement avec Copilot (GitHub) ou Devin mais le reste du code est entièrement développé par moi même mais copilot
|
||||
// reste utile pour la génération de commentaires, de code répétitif ou de code basique (exemple : les formulaires de contact, les requêtes SQL basiques etc...)
|
||||
|
||||
|
||||
// Le header n'y étant pas je rajoute ici un démarrage de session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/functions/security.php');
|
||||
|
||||
// Redirection de l'utilisateur vers une page Home lorsqu'il arrive sur index.php
|
||||
header('location: ./pages/home.php');
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+549
-95
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
// Titre de l'onglet
|
||||
$pageTitle = 'Administration';
|
||||
|
||||
// On ajoute ici le header
|
||||
require_once('./../elements/header.php');
|
||||
|
||||
@@ -7,6 +10,10 @@ require_once '../functions/databaseConnection.php';
|
||||
require_once("../functions/listUser.php");
|
||||
require_once('../functions/listProject.php');
|
||||
require_once('../functions/listXP.php');
|
||||
require_once('../functions/listSchool.php');
|
||||
require_once('../functions/listSkills.php');
|
||||
require_once('../functions/listSettings.php');
|
||||
require_once('../functions/listLogos.php');
|
||||
|
||||
|
||||
// L'utilisateur doit être administrateur
|
||||
@@ -18,74 +25,159 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
// On met les variables à null ici pour éviter qu'il garde les options en premier chargement
|
||||
$projetAModifier = null;
|
||||
$xpAModifier = null;
|
||||
$ecoleAModifier = null;
|
||||
$skillAModifier = null;
|
||||
$logoAModifier = null;
|
||||
|
||||
|
||||
// Si on modifie XP ou Projets
|
||||
// Si on modifie XP, Projets, École ou Compétence
|
||||
if (isset($_GET['edit_project']) && is_numeric($_GET['edit_project'])) {
|
||||
$projetAModifier = recupererProjetParId((int) $_GET['edit_project']);
|
||||
}
|
||||
if (isset($_GET['edit_xp']) && is_numeric($_GET['edit_xp'])) {
|
||||
$xpAModifier = recupererXPParId((int) $_GET['edit_xp']);
|
||||
}
|
||||
if (isset($_GET['edit_school']) && is_numeric($_GET['edit_school'])) {
|
||||
$ecoleAModifier = recupererEcoleParId((int) $_GET['edit_school']);
|
||||
}
|
||||
if (isset($_GET['edit_skill']) && is_numeric($_GET['edit_skill'])) {
|
||||
$skillAModifier = recupererSkillParId((int) $_GET['edit_skill']);
|
||||
}
|
||||
if (isset($_GET['edit_logo']) && is_numeric($_GET['edit_logo'])) {
|
||||
$logoAModifier = recupererLogoParId((int) $_GET['edit_logo']);
|
||||
}
|
||||
|
||||
// Valeurs actuelles de la notification de la page d'accueil
|
||||
$noticeTitle = recupererSetting('site_notice_title') ?? '';
|
||||
$noticeText = recupererSetting('site_notice_text') ?? '';
|
||||
$noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
|
||||
// Valeur actuelle du captcha anti-robot (activé par défaut)
|
||||
$captchaEnabled = recupererSetting('captcha_enabled') ?? '1';
|
||||
|
||||
// Valeurs actuelles des coordonnées de la page contact
|
||||
$contactEmail = recupererSetting('contact_email') ?? 'esteban.moussu@ik.me';
|
||||
$contactEmailEnabled = recupererSetting('contact_email_enabled') ?? '1';
|
||||
$contactAddressLine1 = recupererSetting('contact_address_line1') ?? '20 Grande rue';
|
||||
$contactAddressLine2 = recupererSetting('contact_address_line2') ?? '52500 Broncourt';
|
||||
$contactAddressEnabled = recupererSetting('contact_address_enabled') ?? '1';
|
||||
|
||||
// Page du panneau d'administration
|
||||
?>
|
||||
|
||||
<div class="admin-forms">
|
||||
<div class="admin-page">
|
||||
|
||||
<!-- Formulaire de création de projet -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<div class="project-header">
|
||||
<h2><?php echo $projetAModifier ? 'Modifier le projet' : 'Nouveau projet'; ?></h2>
|
||||
</div>
|
||||
|
||||
<!-- en envoie les données à addProject.php -->
|
||||
<form method="POST" action="<?php echo $projetAModifier ? '../scripts/updateProject.php' : '../scripts/addProject.php'; ?>">
|
||||
|
||||
<?php if ($projetAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<label>Titre *</label>
|
||||
<input type="text" name="title" required value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['title']) : ''; ?>">
|
||||
|
||||
<label>Tags (virgules)</label>
|
||||
<input type="text" name="tags" value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['tags']) : ''; ?>">
|
||||
|
||||
<label>Description *</label>
|
||||
<textarea name="description" required><?php echo $projetAModifier ? htmlspecialchars($projetAModifier['description']) : ''; ?></textarea>
|
||||
|
||||
<label>Lien de l'image *</label>
|
||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['image_url']) : ''; ?>">
|
||||
|
||||
<input type="submit" name="submitproject" value="<?php echo $projetAModifier ? 'Modifier le projet' : '+ Ajouter le projet'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- Titre du panneau -->
|
||||
<div class="page-header">
|
||||
<h1>Panneau d'administration</h1>
|
||||
<p>Gère le contenu du site : projets, expériences, écoles, compétences, logos, notification et utilisateurs.</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de création d'expérience -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<div class="project-header">
|
||||
<h2><?php echo $xpAModifier ? 'Modifier l\'experience' : 'Nouvelle expérience'; ?></h2>
|
||||
</div>
|
||||
<!-- Sous-navigation : accès rapide à chaque catégorie -->
|
||||
<nav class="admin-nav">
|
||||
<a href="#admin-projets"><i class="fa-solid fa-folder-open"></i> Projets</a>
|
||||
<a href="#admin-experiences"><i class="fa-solid fa-briefcase"></i> Expériences</a>
|
||||
<a href="#admin-ecoles"><i class="fa-solid fa-graduation-cap"></i> Écoles</a>
|
||||
<a href="#admin-competences"><i class="fa-solid fa-code"></i> Compétences</a>
|
||||
<a href="#admin-logos"><i class="fa-solid fa-images"></i> Logos</a>
|
||||
<a href="#admin-contact"><i class="fa-solid fa-address-card"></i> Contact</a>
|
||||
<a href="#admin-notification"><i class="fa-solid fa-bell"></i> Notification</a>
|
||||
<a href="#admin-securite"><i class="fa-solid fa-shield-halved"></i> Sécurité</a>
|
||||
<a href="#admin-utilisateurs"><i class="fa-solid fa-users"></i> Utilisateurs</a>
|
||||
<a href="#admin-donnees"><i class="fa-solid fa-database"></i> Données</a>
|
||||
</nav>
|
||||
|
||||
<!-- en envoie les données à addXP.php -->
|
||||
<form method="POST" action="<?php echo $xpAModifier ? '../scripts/updateXP.php' : '../scripts/addXP.php'; ?>">
|
||||
|
||||
<?php if ($xpAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
|
||||
<?php endif; ?>
|
||||
<!-- ========== SECTION PROJETS ========== -->
|
||||
<section id="admin-projets" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-folder-open"></i> Projets</h2>
|
||||
|
||||
<!-- Formulaire (replié par défaut, ouvert automatiquement en mode modification) -->
|
||||
<details class="admin-block" <?php echo $projetAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $projetAModifier ? 'Modifier le projet : ' . htmlspecialchars($projetAModifier['title']) : '+ Nouveau projet'; ?></summary>
|
||||
|
||||
<div class="project-form-card">
|
||||
<!-- enctype multipart/form-data : nécessaire pour l'upload du PDF -->
|
||||
<!-- en envoie les données à addProject.php -->
|
||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $projetAModifier ? '../scripts/updateProject.php' : '../scripts/addProject.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($projetAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<label>Titre *</label>
|
||||
<input type="text" name="title" required value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['title']) : ''; ?>">
|
||||
|
||||
<label>Status (optionnel, affiché sur l'image : En cours, Terminé...)</label>
|
||||
<input type="text" name="status" placeholder="En cours..." value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['status'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Tags (virgules)</label>
|
||||
<input type="text" name="tags" value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['tags']) : ''; ?>">
|
||||
|
||||
<label>Description *</label>
|
||||
<textarea name="description" required><?php echo $projetAModifier ? htmlspecialchars($projetAModifier['description']) : ''; ?></textarea>
|
||||
|
||||
<label>Lien de l'image *</label>
|
||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['image_url']) : ''; ?>">
|
||||
|
||||
<label>Page détaillée en markdown (optionnel, prioritaire sur le PDF et le lien externe)</label>
|
||||
<textarea name="markdown" class="markdown-textarea" placeholder="# Mon projet Description complète en **markdown**..."><?php echo $projetAModifier ? htmlspecialchars($projetAModifier['markdown'] ?? '') : ''; ?></textarea>
|
||||
|
||||
<label>Document PDF (optionnel, utilisé si pas de markdown)</label>
|
||||
<?php if ($projetAModifier && !empty($projetAModifier['pdf_filename'])): ?>
|
||||
<div class="pdf-current">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span><?php echo htmlspecialchars($projetAModifier['pdf_filename']); ?></span>
|
||||
<a href="../scripts/viewPdf.php?type=projet&id=<?php echo (int) $projetAModifier['id']; ?>" target="_blank" rel="noopener noreferrer">Voir</a>
|
||||
<label class="checkbox-row pdf-remove-row">
|
||||
<input type="checkbox" name="remove_pdf"> Supprimer ce PDF
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="pdf-dropzone" data-input="pdf-project">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<p>Glisse un PDF ici, ou <span class="pdf-dropzone-browse">clique pour choisir un fichier</span></p>
|
||||
<span class="pdf-dropzone-filename"></span>
|
||||
<input type="file" name="pdf" id="pdf-project" accept="application/pdf,.pdf" hidden>
|
||||
</div>
|
||||
|
||||
<label>Lien externe (optionnel, utilisé si pas de markdown ni de PDF)</label>
|
||||
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['link_url'] ?? '') : ''; ?>">
|
||||
|
||||
<input type="submit" name="submitproject" value="<?php echo $projetAModifier ? 'Modifier le projet' : '+ Ajouter le projet'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des projets avec modification / suppression -->
|
||||
<?php afficherProjetsAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION EXPÉRIENCES ========== -->
|
||||
<section id="admin-experiences" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-briefcase"></i> Expériences</h2>
|
||||
|
||||
<details class="admin-block" <?php echo $xpAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $xpAModifier ? 'Modifier l\'expérience : ' . htmlspecialchars($xpAModifier['title']) : '+ Nouvelle expérience'; ?></summary>
|
||||
|
||||
<div class="project-form-card">
|
||||
<!-- en envoie les données à addXP.php -->
|
||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $xpAModifier ? '../scripts/updateXP.php' : '../scripts/addXP.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($xpAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<label>Titre *</label>
|
||||
<input type="text" name="title" required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['title']) : ''; ?>">
|
||||
|
||||
<label>Status (optionnel, affiché sur l'image : En cours, Terminé...)</label>
|
||||
<input type="text" name="status" placeholder="En cours..." value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['status'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Type *</label>
|
||||
<textarea name="xptype" required><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['xptype']) : ''; ?></textarea>
|
||||
<input type="text" name="xptype" required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['xptype']) : ''; ?>">
|
||||
|
||||
<label>Tags (virgules)</label>
|
||||
<input type="text" name="tags" value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['tags']) : ''; ?>">
|
||||
@@ -94,62 +186,424 @@ if (isset($_GET['edit_xp']) && is_numeric($_GET['edit_xp'])) {
|
||||
<textarea name="description" required><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['description']) : ''; ?></textarea>
|
||||
|
||||
<label>Durée *</label>
|
||||
<textarea name="duration" required><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['duration']) : ''; ?></textarea>
|
||||
<input type="text" name="duration" required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['duration']) : ''; ?>">
|
||||
|
||||
<label>Lien de l'image *</label>
|
||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['image_url']) : ''; ?>">
|
||||
|
||||
<label>Page détaillée en markdown (optionnel, prioritaire sur le PDF et le lien externe)</label>
|
||||
<textarea name="markdown" class="markdown-textarea" placeholder="# Mon expérience Description complète en **markdown**..."><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['markdown'] ?? '') : ''; ?></textarea>
|
||||
|
||||
<label>Document PDF (optionnel, utilisé si pas de markdown)</label>
|
||||
<?php if ($xpAModifier && !empty($xpAModifier['pdf_filename'])): ?>
|
||||
<div class="pdf-current">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span><?php echo htmlspecialchars($xpAModifier['pdf_filename']); ?></span>
|
||||
<a href="../scripts/viewPdf.php?type=experience&id=<?php echo (int) $xpAModifier['id']; ?>" target="_blank" rel="noopener noreferrer">Voir</a>
|
||||
<label class="checkbox-row pdf-remove-row">
|
||||
<input type="checkbox" name="remove_pdf"> Supprimer ce PDF
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="pdf-dropzone" data-input="pdf-xp">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<p>Glisse un PDF ici, ou <span class="pdf-dropzone-browse">clique pour choisir un fichier</span></p>
|
||||
<span class="pdf-dropzone-filename"></span>
|
||||
<input type="file" name="pdf" id="pdf-xp" accept="application/pdf,.pdf" hidden>
|
||||
</div>
|
||||
|
||||
<label>Lien externe (optionnel, utilisé si pas de markdown ni de PDF)</label>
|
||||
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['link_url'] ?? '') : ''; ?>">
|
||||
|
||||
<input type="submit" name="submitxp" value="<?php echo $xpAModifier ? 'Modifier l\'experience' : '+ Ajouter l\'experience'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des expériences avec modification / suppression -->
|
||||
<?php afficherXPAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION ÉCOLES ========== -->
|
||||
<section id="admin-ecoles" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-graduation-cap"></i> Écoles</h2>
|
||||
|
||||
<details class="admin-block" <?php echo $ecoleAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $ecoleAModifier ? 'Modifier l\'école : ' . htmlspecialchars($ecoleAModifier['title']) : '+ Nouvelle école'; ?></summary>
|
||||
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à addSchool.php -->
|
||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $ecoleAModifier ? '../scripts/updateSchool.php' : '../scripts/addSchool.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($ecoleAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $ecoleAModifier['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<label>Nom de l'école *</label>
|
||||
<input type="text" name="title" required value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['title']) : ''; ?>">
|
||||
|
||||
<label>Status (optionnel, affiché sur l'image : En cours, Diplômé...)</label>
|
||||
<input type="text" name="status" placeholder="En cours..." value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['status'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Tags (virgules)</label>
|
||||
<input type="text" name="tags" value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['tags']) : ''; ?>">
|
||||
|
||||
<label>Description (diplôme, spécialité...) *</label>
|
||||
<textarea name="description" required><?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['description']) : ''; ?></textarea>
|
||||
|
||||
<label>Période (ex : 2023 - 2025) *</label>
|
||||
<input type="text" name="duration" required value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['duration']) : ''; ?>">
|
||||
|
||||
<label>Lien de l'image *</label>
|
||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['image_url']) : ''; ?>">
|
||||
|
||||
<label>Page détaillée en markdown (optionnel, prioritaire sur le PDF et le lien externe)</label>
|
||||
<textarea name="markdown" class="markdown-textarea" placeholder="# Mon école Description complète en **markdown**..."><?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['markdown'] ?? '') : ''; ?></textarea>
|
||||
|
||||
<label>Document PDF (optionnel, utilisé si pas de markdown)</label>
|
||||
<?php if ($ecoleAModifier && !empty($ecoleAModifier['pdf_filename'])): ?>
|
||||
<div class="pdf-current">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span><?php echo htmlspecialchars($ecoleAModifier['pdf_filename']); ?></span>
|
||||
<a href="../scripts/viewPdf.php?type=ecole&id=<?php echo (int) $ecoleAModifier['id']; ?>" target="_blank" rel="noopener noreferrer">Voir</a>
|
||||
<label class="checkbox-row pdf-remove-row">
|
||||
<input type="checkbox" name="remove_pdf"> Supprimer ce PDF
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="pdf-dropzone" data-input="pdf-school">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<p>Glisse un PDF ici, ou <span class="pdf-dropzone-browse">clique pour choisir un fichier</span></p>
|
||||
<span class="pdf-dropzone-filename"></span>
|
||||
<input type="file" name="pdf" id="pdf-school" accept="application/pdf,.pdf" hidden>
|
||||
</div>
|
||||
|
||||
<label>Lien externe (optionnel, utilisé si pas de markdown ni de PDF)</label>
|
||||
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['link_url'] ?? '') : ''; ?>">
|
||||
|
||||
<input type="submit" name="submitschool" value="<?php echo $ecoleAModifier ? 'Modifier l\'école' : '+ Ajouter l\'école'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des écoles avec modification / suppression -->
|
||||
<?php afficherEcolesAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION COMPÉTENCES ========== -->
|
||||
<section id="admin-competences" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-code"></i> Compétences</h2>
|
||||
|
||||
<details class="admin-block" <?php echo $skillAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $skillAModifier ? 'Modifier la catégorie : ' . htmlspecialchars($skillAModifier['title']) : '+ Nouvelle catégorie de compétences'; ?></summary>
|
||||
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à addSkill.php -->
|
||||
<form method="POST" action="<?php echo $skillAModifier ? '../scripts/updateSkill.php' : '../scripts/addSkill.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($skillAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $skillAModifier['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<label>Titre de la catégorie *</label>
|
||||
<input type="text" name="title" required value="<?php echo $skillAModifier ? htmlspecialchars($skillAModifier['title']) : ''; ?>">
|
||||
|
||||
<label>Icône de la catégorie (classes Font Awesome, ex : fa-solid fa-terminal)</label>
|
||||
<input type="text" name="icon" placeholder="fa-solid fa-star" value="<?php echo $skillAModifier ? htmlspecialchars($skillAModifier['icon']) : ''; ?>">
|
||||
|
||||
<label>Compétences (une par ligne, format : "icone|texte" ou juste "texte") *</label>
|
||||
<textarea name="items" placeholder="fa-brands fa-python|Python fa-solid fa-terminal|Bash Ansible" required><?php echo $skillAModifier ? htmlspecialchars($skillAModifier['items']) : ''; ?></textarea>
|
||||
|
||||
<label>Position (ordre d'affichage, du plus petit au plus grand)</label>
|
||||
<input type="text" name="position" value="<?php echo $skillAModifier ? (int) $skillAModifier['position'] : '0'; ?>">
|
||||
|
||||
<input type="submit" name="submitskill" value="<?php echo $skillAModifier ? 'Modifier la compétence' : '+ Ajouter la compétence'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des compétences avec modification / suppression -->
|
||||
<div class="skills-section skills-section-admin">
|
||||
<?php afficherSkillsAdmin(); ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION LOGOS (bandeaux page d'accueil) ========== -->
|
||||
<section id="admin-logos" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-images"></i> Logos partenaires (page d'accueil)</h2>
|
||||
<p class="admin-data-text admin-section-intro">
|
||||
Le bandeau "Outils que je sais manipuler" est désormais dérivé automatiquement de la
|
||||
page <a href="#admin-competences">Compétences</a> (icône dans le champ "icone|texte" de chaque item).
|
||||
Cette section ne gère plus que le bandeau "Ils m'ont fait confiance".
|
||||
</p>
|
||||
|
||||
<details class="admin-block" <?php echo $logoAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $logoAModifier ? 'Modifier le logo : ' . htmlspecialchars($logoAModifier['name']) : '+ Nouvelle entreprise'; ?></summary>
|
||||
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à addLogo.php -->
|
||||
<form method="POST" action="<?php echo $logoAModifier ? '../scripts/updateLogo.php' : '../scripts/addLogo.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($logoAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $logoAModifier['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Seul le bandeau "Ils m'ont fait confiance" est géré ici -->
|
||||
<input type="hidden" name="category" value="partner">
|
||||
|
||||
<label>Nom de l'entreprise *</label>
|
||||
<input type="text" name="name" required placeholder="Nom de l'entreprise..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['name']) : ''; ?>">
|
||||
|
||||
<label>
|
||||
Icône Devicon (facultatif, rarement utile pour une entreprise — parcourir sur
|
||||
<a href="https://devicon.dev" target="_blank" rel="noopener noreferrer">devicon.dev</a>)
|
||||
</label>
|
||||
<input type="text" name="icon" placeholder="devicon-docker-plain colored" value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['icon'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Lien de l'image (utilisé si pas d'icône)</label>
|
||||
<input type="url" name="logourl" placeholder="https://..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['logo_url'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Lien externe (optionnel, rend le logo cliquable)</label>
|
||||
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['link_url'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Position (ordre d'affichage, du plus petit au plus grand)</label>
|
||||
<input type="text" name="position" value="<?php echo $logoAModifier ? (int) $logoAModifier['position'] : '0'; ?>">
|
||||
|
||||
<input type="submit" name="submitlogo" value="<?php echo $logoAModifier ? 'Modifier le logo' : '+ Ajouter le logo'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des logos avec modification / suppression -->
|
||||
<?php afficherLogosAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION CONTACT ========== -->
|
||||
<section id="admin-contact" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-address-card"></i> Coordonnées de contact</h2>
|
||||
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="hidden" name="settings_group" value="contact">
|
||||
|
||||
<label>Adresse email</label>
|
||||
<input type="email" name="contact_email" value="<?php echo htmlspecialchars($contactEmail); ?>">
|
||||
|
||||
<!-- Checkbox pour afficher / masquer l'email sur la page contact -->
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="contact_email_enabled" value="1" <?php echo $contactEmailEnabled === '1' ? 'checked' : ''; ?>>
|
||||
Afficher l'email sur la page contact
|
||||
</label>
|
||||
|
||||
<label>Adresse postale - ligne 1 (rue)</label>
|
||||
<input type="text" name="contact_address_line1" value="<?php echo htmlspecialchars($contactAddressLine1); ?>">
|
||||
|
||||
<label>Adresse postale - ligne 2 (code postal / ville)</label>
|
||||
<input type="text" name="contact_address_line2" value="<?php echo htmlspecialchars($contactAddressLine2); ?>">
|
||||
|
||||
<!-- Checkbox pour afficher / masquer l'adresse postale sur la page contact -->
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="contact_address_enabled" value="1" <?php echo $contactAddressEnabled === '1' ? 'checked' : ''; ?>>
|
||||
Afficher l'adresse postale sur la page contact
|
||||
</label>
|
||||
|
||||
<input type="submit" name="submitsettings" value="Enregistrer les coordonnées" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION NOTIFICATION ========== -->
|
||||
<section id="admin-notification" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-bell"></i> Notification de la page d'accueil</h2>
|
||||
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="hidden" name="settings_group" value="notice">
|
||||
|
||||
<label>Titre (affiché en gras)</label>
|
||||
<input type="text" name="notice_title" value="<?php echo htmlspecialchars($noticeTitle); ?>">
|
||||
|
||||
<label>Texte</label>
|
||||
<textarea name="notice_text"><?php echo htmlspecialchars($noticeText); ?></textarea>
|
||||
|
||||
<!-- Checkbox pour activer / désactiver la notification sans la supprimer -->
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="notice_enabled" value="1" <?php echo $noticeEnabled === '1' ? 'checked' : ''; ?>>
|
||||
Afficher la notification sur la page d'accueil
|
||||
</label>
|
||||
|
||||
<input type="submit" name="submitsettings" value="Enregistrer la notification" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION SÉCURITÉ ========== -->
|
||||
<section id="admin-securite" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-shield-halved"></i> Sécurité</h2>
|
||||
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="hidden" name="settings_group" value="captcha">
|
||||
|
||||
<!-- Checkbox pour activer / désactiver le test anti-robot à l'entrée du site -->
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="captcha_enabled" value="1" <?php echo $captchaEnabled === '1' ? 'checked' : ''; ?>>
|
||||
Activer le test anti-robot (captcha) à l'entrée du site
|
||||
</label>
|
||||
|
||||
<input type="submit" name="submitsettings" value="Enregistrer" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION UTILISATEURS ========== -->
|
||||
<section id="admin-utilisateurs" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-users"></i> Utilisateurs</h2>
|
||||
|
||||
<details class="admin-block">
|
||||
<summary>+ Nouveau compte</summary>
|
||||
|
||||
<div class="project-form-card">
|
||||
<!-- en envoie les données addUser.php -->
|
||||
<form method="POST" action="../scripts/addUser.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<label>Nom du compte</label>
|
||||
<input type="text" name="user" required>
|
||||
|
||||
<!-- role admin ou user -->
|
||||
<label>Rôle (admin/user)</label>
|
||||
<input type="text" name="role" placeholder="user" required>
|
||||
|
||||
<!-- mot de passe -->
|
||||
<label>Mot de passe</label>
|
||||
<input type="password" name="passwd" required>
|
||||
|
||||
<!-- Le bouton qui déclenche le PHP en haut de page -->
|
||||
<input type="submit" name="submituser" value="Ajouter l'utilisateur" class="btn-submit">
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Affichage de la liste des utilisateurs -->
|
||||
<?php afficherUsersAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION DONNÉES (EXPORT / IMPORT) ========== -->
|
||||
<section id="admin-donnees" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-database"></i> Données du site</h2>
|
||||
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<!-- Export : télécharge toutes les données en JSON -->
|
||||
<h3 class="admin-data-title"><i class="fa-solid fa-download"></i> Exporter</h3>
|
||||
<p class="admin-data-text">
|
||||
Télécharge une sauvegarde JSON de toutes les données du site
|
||||
(projets, expériences, écoles, compétences, notification, utilisateurs).
|
||||
</p>
|
||||
<a class="btn-submit admin-export-btn" href="../scripts/exportData.php?csrf_token=<?php echo urlencode(csrfToken()); ?>">
|
||||
<i class="fa-solid fa-download"></i> Exporter les données (JSON)
|
||||
</a>
|
||||
|
||||
<hr class="admin-data-separator">
|
||||
|
||||
<!-- Import : écrase les données actuelles avec celles du fichier -->
|
||||
<h3 class="admin-data-title"><i class="fa-solid fa-upload"></i> Importer</h3>
|
||||
<p class="admin-data-text admin-data-warning">
|
||||
⚠️ L'import <strong>écrase définitivement</strong> les données actuelles des tables
|
||||
présentes dans le fichier. Fais un export avant, au cas où !
|
||||
</p>
|
||||
<form method="POST" action="../scripts/importData.php" enctype="multipart/form-data"
|
||||
onsubmit="return confirm('ATTENTION : les données actuelles du site vont être écrasées par celles du fichier. Continuer ?');">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="file" name="jsonfile" accept=".json,application/json" required>
|
||||
<input type="submit" value="Importer les données" class="btn-submit btn-delete">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Gestion des zones de drag & drop PDF (projets, expériences, écoles) -->
|
||||
<script>
|
||||
(function () {
|
||||
document.querySelectorAll('.pdf-dropzone').forEach(function (zone) {
|
||||
var input = document.getElementById(zone.dataset.input);
|
||||
var filenameEl = zone.querySelector('.pdf-dropzone-filename');
|
||||
|
||||
function afficherFichier(file) {
|
||||
if (!file) return;
|
||||
if (file.type !== 'application/pdf') {
|
||||
filenameEl.textContent = 'Ce fichier n\'est pas un PDF.';
|
||||
zone.classList.remove('has-file');
|
||||
return;
|
||||
}
|
||||
filenameEl.textContent = file.name;
|
||||
zone.classList.add('has-file');
|
||||
}
|
||||
|
||||
// Clic sur la zone = ouvre le sélecteur de fichier
|
||||
// Garde-fou : input.click() déclenche lui-même un évènement "click" qui remonte
|
||||
// (bubble) jusqu'à la zone puisque l'input est à l'intérieur de celle-ci. Sans ce
|
||||
// test, ce clic remonté relance input.click() une seconde fois => double sélecteur.
|
||||
zone.addEventListener('click', function (e) {
|
||||
if (e.target === input) return;
|
||||
input.click();
|
||||
});
|
||||
|
||||
input.addEventListener('change', function () {
|
||||
afficherFichier(input.files[0]);
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach(function (evt) {
|
||||
zone.addEventListener(evt, function (e) {
|
||||
e.preventDefault();
|
||||
zone.classList.add('dragging');
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(function (evt) {
|
||||
zone.addEventListener(evt, function (e) {
|
||||
e.preventDefault();
|
||||
zone.classList.remove('dragging');
|
||||
});
|
||||
});
|
||||
|
||||
zone.addEventListener('drop', function (e) {
|
||||
var file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
input.files = e.dataTransfer.files;
|
||||
afficherFichier(file);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// Liste des projets / xps => en gros on pourra supprimer, et modifier à la vollée sans aller dans la bdd
|
||||
?>
|
||||
<h2 class="projects-title">Gérer les projets</h2>
|
||||
<?php afficherProjetsAdmin(); ?>
|
||||
<h2 class="projects-title">Gérer les experiences</h2>
|
||||
<?php afficherXPAdmin();
|
||||
|
||||
// Création de compte
|
||||
?>
|
||||
|
||||
<!-- Formulaire de création de compte -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
<div class="project-header">
|
||||
<h2>Nouveau compte</h2>
|
||||
</div>
|
||||
|
||||
<!-- en envoie les données addUser.php -->
|
||||
<form method="POST" action="../scripts/addUser.php">
|
||||
<label>Nom du compte</label>
|
||||
<input type="text" name="user" required>
|
||||
|
||||
<!-- role admin ou user -->
|
||||
<label>Rôle (admin/user)</label>
|
||||
<textarea name="role" required></textarea>
|
||||
|
||||
<!-- mot de passe -->
|
||||
<label>Mot de passe</label>
|
||||
<input type="password" name="passwd" required>
|
||||
|
||||
<!-- Le bouton qui déclenche le PHP en haut de page -->
|
||||
<input type="submit" name="submituser" value="Ajouter l'utilisateur" class="btn-submit">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des utilisateurs -->
|
||||
<h2 class="projects-title">Gérer les utilisateurs</h2>
|
||||
|
||||
<!-- Affichage de la liste des utilisateurs -->
|
||||
<?php
|
||||
afficherUsersAdmin();
|
||||
|
||||
// On ajoute ici le footer
|
||||
require_once('../elements/footer.php'); ?>
|
||||
require_once('../elements/footer.php'); ?>
|
||||
|
||||
+6
-2
@@ -1,11 +1,15 @@
|
||||
<?php
|
||||
|
||||
// CGU / CDU
|
||||
|
||||
// Titre de l'onglet
|
||||
$pageTitle = 'Conditions générales d\'utilisation';
|
||||
|
||||
require_once('./../elements/header.php');
|
||||
|
||||
?>
|
||||
|
||||
<main class="cgu-page">
|
||||
<div class="cgu-page">
|
||||
<section class="cgu-card">
|
||||
<h1> Conditions générales d'utilisation </h1>
|
||||
<h2> Dernière mise à jour : 29 juin 2026</h2>
|
||||
@@ -57,7 +61,7 @@ require_once('./../elements/header.php');
|
||||
<h3> 12. Droit applicable</h3>
|
||||
<p>Les présentes CGU sont soumises au droit français. En cas de litige, et à défaut de résolution amiable, les juridictions compétentes seront celles déterminées par les règles de droit commun.</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once('../elements/footer.php');
|
||||
|
||||
+44
-28
@@ -1,41 +1,57 @@
|
||||
<?php
|
||||
// Formulaire de contact avec WEBHOOK Discord
|
||||
|
||||
// Titre de l'onglet
|
||||
$pageTitle = 'Contact';
|
||||
|
||||
// On ajoute ici le header
|
||||
require_once("../elements/header.php");
|
||||
|
||||
// Réglages de l'email et de l'adresse (modifiables et masquables depuis l'admin)
|
||||
require_once('../functions/listSettings.php');
|
||||
|
||||
$contactEmail = recupererSetting('contact_email') ?? 'esteban.moussu@ik.me';
|
||||
$contactEmailEnabled = (recupererSetting('contact_email_enabled') ?? '1') === '1';
|
||||
|
||||
$contactAddressLine1 = recupererSetting('contact_address_line1') ?? '20 Grande rue';
|
||||
$contactAddressLine2 = recupererSetting('contact_address_line2') ?? '52500 Broncourt';
|
||||
$contactAddressEnabled = (recupererSetting('contact_address_enabled') ?? '1') === '1';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<main class="contact-page">
|
||||
<section class="contact-wrapper">
|
||||
<div class="contact-card">
|
||||
<div class="contact-info">
|
||||
<p class="contact-title">Contact</p>
|
||||
<p class="contact-name">Moussu Esteban</p>
|
||||
<p class="contact-line">esteban.moussu@ik.me</p>
|
||||
<p class="contact-line">20 Grande rue</p>
|
||||
<p class="contact-line">52500 Broncourt</p>
|
||||
<div class="contact-page">
|
||||
<section class="contact-wrapper">
|
||||
<div class="contact-card">
|
||||
<div class="contact-info">
|
||||
<p class="contact-title">Contact</p>
|
||||
<p class="contact-name">Esteban MOUSSU</p>
|
||||
<?php if ($contactEmailEnabled && !empty($contactEmail)): ?>
|
||||
<p class="contact-line"><i class="fa-solid fa-envelope"></i> <?php echo htmlspecialchars($contactEmail); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($contactAddressEnabled && (!empty($contactAddressLine1) || !empty($contactAddressLine2))): ?>
|
||||
<p class="contact-line"><i class="fa-solid fa-location-dot"></i> <?php echo htmlspecialchars($contactAddressLine1); ?></p>
|
||||
<p class="contact-line"><?php echo htmlspecialchars($contactAddressLine2); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'envoi -->
|
||||
<form class="contact-form" method="POST" action="../scripts/sendMessage.php">
|
||||
<!-- Deux champs de texte, un subject et un message -->
|
||||
<div class="contact-grid">
|
||||
<input type="text" name="subject" id="subject" placeholder="Sujet du message" required>
|
||||
<input type="text" name="name" id="name" placeholder="Nom et prénom" required>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'envoi -->
|
||||
<form class="contact-form" method="POST" action="../scripts/sendMessage.php">
|
||||
<!-- Deux champs de texte, un subject et un message -->
|
||||
<div class="contact-grid">
|
||||
<input type="text" name="subject" id="subject" placeholder="Sujet du message" required>
|
||||
<input type="text" name="name" id="name" placeholder="Nom et prénom" required>
|
||||
</div>
|
||||
<input type="email" name="mail" id="mail" placeholder="E-mail de contact" required>
|
||||
<textarea name="message" id="message" placeholder="Votre message" required></textarea>
|
||||
|
||||
<input type="email" name="mail" id="mail" placeholder="E-mail de contact" required>
|
||||
<textarea name="message" id="message" placeholder="Votre message" required></textarea>
|
||||
|
||||
<!-- Bouton d'envoi -->
|
||||
<input type="submit" value="Envoyer le message">
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
<!-- Bouton d'envoi -->
|
||||
<input type="submit" value="Envoyer le message">
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Ajout du footer
|
||||
require_once("../elements/footer.php");
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
// Page de détails d'un projet / d'une expérience / d'une école
|
||||
// Le contenu markdown (stocké en BDD, éditable depuis le panel admin) est rendu
|
||||
// côté navigateur avec marked.js puis nettoyé avec DOMPurify (sécurité XSS)
|
||||
|
||||
// Connexion BDD (avant le header car on doit pouvoir rediriger si l'élément n'existe pas)
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Types autorisés : liste blanche pour empêcher toute injection de nom de table
|
||||
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6) : on la
|
||||
// joint juste pour le nom de fichier, jamais pour son contenu binaire)
|
||||
$types = [
|
||||
'projet' => ['table' => 'projects', 'pdf' => 'project_pdfs', 'pdf_colonne' => 'project_id', 'retour' => './projects.php', 'label' => 'Projet'],
|
||||
'experience' => ['table' => 'experiences', 'pdf' => 'experience_pdfs', 'pdf_colonne' => 'experience_id', 'retour' => './xp.php', 'label' => 'Expérience'],
|
||||
'ecole' => ['table' => 'schools', 'pdf' => 'school_pdfs', 'pdf_colonne' => 'school_id', 'retour' => './xp.php', 'label' => 'École'],
|
||||
];
|
||||
|
||||
$type = $_GET['type'] ?? '';
|
||||
$id = $_GET['id'] ?? '';
|
||||
|
||||
// Type inconnu ou id invalide : retour à l'accueil
|
||||
if (!isset($types[$type]) || !is_numeric($id)) {
|
||||
header('Location: ./home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$infos = $types[$type];
|
||||
|
||||
// Récupération de l'élément demandé
|
||||
// (pdf_data n'est volontairement pas sélectionné : le PDF est servi à part par viewPdf.php
|
||||
// dans une <iframe>, inutile de charger son contenu binaire en mémoire ici)
|
||||
$colonnesCommunes = 'e.id, e.title, e.status, e.tags, e.description, e.image_url, e.markdown, e.link_url, p.pdf_filename';
|
||||
$colonnesParType = [
|
||||
'projet' => $colonnesCommunes,
|
||||
'experience' => $colonnesCommunes . ', e.xptype, e.duration',
|
||||
'ecole' => $colonnesCommunes . ', e.duration',
|
||||
];
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare(
|
||||
'SELECT ' . $colonnesParType[$type] . '
|
||||
FROM `' . $infos['table'] . '` e
|
||||
LEFT JOIN `' . $infos['pdf'] . '` p ON p.`' . $infos['pdf_colonne'] . '` = e.id
|
||||
WHERE e.id = :id'
|
||||
);
|
||||
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$element = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
$element = false;
|
||||
}
|
||||
|
||||
$hasMarkdown = $element && trim($element['markdown'] ?? '') !== '';
|
||||
$hasPdf = $element && trim($element['pdf_filename'] ?? '') !== '';
|
||||
|
||||
// Élément inexistant, ou sans markdown ni PDF : retour à la liste
|
||||
if (!$element || (!$hasMarkdown && !$hasPdf)) {
|
||||
header('Location: ' . $infos['retour']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Titre de l'onglet
|
||||
$pageTitle = $element['title'];
|
||||
|
||||
// On ajoute le header
|
||||
require_once('../elements/header.php');
|
||||
|
||||
// Préparation des données pour l'affichage
|
||||
$titre = htmlspecialchars($element['title']);
|
||||
$image = htmlspecialchars($element['image_url'] ?? '');
|
||||
$status = htmlspecialchars($element['status'] ?? '');
|
||||
$duration = htmlspecialchars($element['duration'] ?? '');
|
||||
$xptype = htmlspecialchars($element['xptype'] ?? '');
|
||||
$tags = [];
|
||||
if (!empty($element['tags'])) {
|
||||
$tags = array_filter(array_map('trim', explode(',', $element['tags'])));
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="details-page">
|
||||
|
||||
<!-- Bouton retour vers la liste -->
|
||||
<a class="btn btn-ghost details-back" href="<?php echo $infos['retour']; ?>">
|
||||
<i class="fa-solid fa-arrow-left"></i> Retour
|
||||
</a>
|
||||
|
||||
<article class="details-card">
|
||||
|
||||
<?php if (!empty($image)): ?>
|
||||
<div class="details-banner project-banner">
|
||||
<img src="<?php echo $image; ?>" alt="Image : <?php echo $titre; ?>">
|
||||
<?php if (!empty($status)): ?>
|
||||
<div class="project-state"><?php echo $status; ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="details-content">
|
||||
|
||||
<span class="details-type"><?php echo $infos['label']; ?></span>
|
||||
<h1><?php echo $titre; ?></h1>
|
||||
|
||||
<!-- Métadonnées : type / durée si renseignés -->
|
||||
<?php if (!empty($xptype) || !empty($duration)): ?>
|
||||
<p class="details-meta">
|
||||
<?php if (!empty($xptype)): ?><span><i class="fa-solid fa-tag"></i> <?php echo $xptype; ?></span><?php endif; ?>
|
||||
<?php if (!empty($duration)): ?><span><i class="fa-solid fa-clock"></i> <?php echo $duration; ?></span><?php endif; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($tags)): ?>
|
||||
<div class="project-tags">
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
<span class="tag"><?php echo htmlspecialchars($tag); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasMarkdown): ?>
|
||||
<!-- Le markdown brut est placé ici (échappé), puis rendu en HTML par le script plus bas -->
|
||||
<div id="markdown-source" hidden><?php echo htmlspecialchars($element['markdown']); ?></div>
|
||||
<div class="markdown-content" id="markdown-target">
|
||||
<p>Chargement du contenu...</p>
|
||||
</div>
|
||||
<?php elseif ($hasPdf): ?>
|
||||
<!-- Mini lecteur PDF : le fichier est servi par viewPdf.php depuis la base de données -->
|
||||
<div class="pdf-viewer-wrap">
|
||||
<div class="pdf-viewer-toolbar">
|
||||
<span><i class="fa-solid fa-file-pdf"></i> <?php echo htmlspecialchars($element['pdf_filename']); ?></span>
|
||||
<a class="btn btn-ghost pdf-download-btn" href="../scripts/viewPdf.php?type=<?php echo $type; ?>&id=<?php echo (int) $id; ?>&download=1" download>
|
||||
<i class="fa-solid fa-download"></i> Télécharger
|
||||
</a>
|
||||
</div>
|
||||
<iframe
|
||||
class="pdf-viewer"
|
||||
src="../scripts/viewPdf.php?type=<?php echo $type; ?>&id=<?php echo (int) $id; ?>"
|
||||
title="Aperçu PDF : <?php echo $titre; ?>"
|
||||
></iframe>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($hasMarkdown): ?>
|
||||
<!-- marked.js : convertit le markdown en HTML / DOMPurify : nettoie le HTML généré (sécurité) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var source = document.getElementById('markdown-source');
|
||||
var cible = document.getElementById('markdown-target');
|
||||
// textContent décode automatiquement les entités HTML => on récupère le markdown d'origine
|
||||
var markdown = source.textContent;
|
||||
cible.innerHTML = DOMPurify.sanitize(marked.parse(markdown, { breaks: true }));
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// Ajout du footer
|
||||
require_once('../elements/footer.php'); ?>
|
||||
+81
-11
@@ -3,28 +3,88 @@
|
||||
// Page Home PortFolio Esteban
|
||||
// Cette page servira de page d'accueil où nous pourrons y retrouver une redirection vers les différents projets, espace de connexion, form de contact, etc...
|
||||
|
||||
// Titre de l'onglet
|
||||
$pageTitle = 'Accueil';
|
||||
|
||||
// Insertion du header
|
||||
require_once('./../elements/header.php');
|
||||
|
||||
// Ajout d'un texte pour rappeller aux utilisateurs de la démo de vider le cache si du CSS s'affiche mal
|
||||
// La notification est maintenant dynamique : modifiable depuis le panel Admin (table settings)
|
||||
require_once('../functions/listSettings.php');
|
||||
|
||||
//Si du CSS / ou des images s'affichent mal, il faut penser à vider le cache, version de développement uniquement.
|
||||
// Bandeau "Ils m'ont fait confiance", géré depuis le panel Admin (table logo_banners)
|
||||
require_once('../functions/listLogos.php');
|
||||
|
||||
// Bandeau "Outils que je sais manipuler" : dérivé directement des icônes de la page
|
||||
// Compétences, pas de saisie séparée (voir listeIconesCompetences)
|
||||
require_once('../functions/listSkills.php');
|
||||
|
||||
// Chiffres clés (bandeau de stats sous le hero) : nombre réel de projets/expériences
|
||||
// en BDD, et nombre de technologies reconnaissables (réutilise listeIconesCompetences)
|
||||
require_once('../functions/listStats.php');
|
||||
$statsHome = recupererStatsHome();
|
||||
$statsHome['technologies'] = count(listeIconesCompetences());
|
||||
|
||||
// Affichage de la notification (si elle est activée dans le panel Admin)
|
||||
afficherNotice();
|
||||
?>
|
||||
<!-- Message d'alerte pour les utilisateurs de la démo -->
|
||||
<p class="dev-cache-notice">
|
||||
<strong>Version de développement uniquement.</strong>
|
||||
Si le CSS ou les images s'affichent mal, pensez à vider le cache de votre navigateur
|
||||
(Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications.
|
||||
</p>
|
||||
|
||||
<!-- Section hero : présentation rapide -->
|
||||
<section class="hero">
|
||||
<span class="hero-badge"><i class="fa-solid fa-graduation-cap"></i> Étudiant en informatique</span>
|
||||
<h1>Salut, moi c'est <span class="accent">Esteban</span></h1>
|
||||
<p>
|
||||
Passionné par les systèmes, les réseaux et le développement web.
|
||||
Découvre mes projets, mes expériences professionnelles et mes compétences.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="../pages/projects.php" class="btn btn-primary"><i class="fa-solid fa-folder-open"></i> Voir mes projets</a>
|
||||
<a href="../pages/contact.php" class="btn btn-ghost"><i class="fa-solid fa-paper-plane"></i> Me contacter</a>
|
||||
</div>
|
||||
|
||||
<!-- Flèche animée qui invite à défiler vers le bas -->
|
||||
<a href="#explorer" class="scroll-indicator" aria-label="Défiler vers le bas">
|
||||
<i class="fa-solid fa-chevron-down"></i>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<?php
|
||||
// Chiffres clés : n'affiche que les compteurs non nuls (pas de "0 projets" décevant
|
||||
// si le site vient d'être initialisé)
|
||||
$statsAffichables = array_filter([
|
||||
'projets' => ['valeur' => $statsHome['projets'], 'label' => 'Projet' . ($statsHome['projets'] > 1 ? 's' : '') . ' réalisés'],
|
||||
'experiences' => ['valeur' => $statsHome['experiences'], 'label' => 'Expérience' . ($statsHome['experiences'] > 1 ? 's' : '') . ' pro'],
|
||||
'technologies' => ['valeur' => $statsHome['technologies'], 'label' => 'Technologies maîtrisées'],
|
||||
], function ($stat) { return $stat['valeur'] > 0; });
|
||||
?>
|
||||
<?php if (!empty($statsAffichables)): ?>
|
||||
<!-- Bandeau de chiffres clés : compteurs réels, calculés depuis la BDD -->
|
||||
<section class="stats-strip">
|
||||
<?php foreach ($statsAffichables as $stat): ?>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><?php echo (int) $stat['valeur']; ?></span>
|
||||
<span class="stat-label"><?php echo htmlspecialchars($stat['label']); ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty(listeIconesCompetences())): ?>
|
||||
<!-- Bandeau défilant des outils, dérivé de la page Compétences -->
|
||||
<section class="logo-banner-section">
|
||||
<div class="logo-banner-header">
|
||||
<h2>Outils que je sais manipuler</h2>
|
||||
</div>
|
||||
<?php afficherOutilsCompetences(); ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Section d'exploration rapide -->
|
||||
<section class="quick-links-section">
|
||||
<section class="quick-links-section" id="explorer">
|
||||
<div class="quick-links-header">
|
||||
<!-- Titre et description de la section -->
|
||||
<h2>Explorer</h2>
|
||||
<p>Accède rapidement aux sections principales du portfolio.</p>
|
||||
<p>Accède rapidement aux sections principales de mon portfolio.</p>
|
||||
</div>
|
||||
<!-- Grille de cartes de redirection vers les différentes page -->
|
||||
<div class="quick-links-grid">
|
||||
@@ -50,7 +110,7 @@ require_once('./../elements/header.php');
|
||||
</div>
|
||||
<h3>Compétences</h3>
|
||||
<!-- Page où il y aura toutes mes compétences -->
|
||||
<p>PAS ENCORE DEV</p>
|
||||
<p>Systèmes, réseaux et développement.</p>
|
||||
</a>
|
||||
|
||||
<a href="../pages/contact.php" class="quick-link-card">
|
||||
@@ -63,6 +123,16 @@ require_once('./../elements/header.php');
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (!empty(listeLogos('partner'))): ?>
|
||||
<!-- Bandeau défilant des entreprises partenaires, géré depuis le panel Admin -->
|
||||
<section class="logo-banner-section">
|
||||
<div class="logo-banner-header">
|
||||
<h2>Ils m'ont fait confiance</h2>
|
||||
</div>
|
||||
<?php afficherLogos('partner'); ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// Ajout du footer
|
||||
require_once('../elements/footer.php');
|
||||
|
||||
+3
-6
@@ -3,7 +3,8 @@
|
||||
// Dans l'idée un champ pour un nom d'utilisateur, et un pour le mot de passe puis un bouton de connexion
|
||||
// Egalement permettre à l'utilisateur de faire retour en arrière s'il s'est trompé (croix ou header)
|
||||
|
||||
|
||||
// Titre de l'onglet
|
||||
$pageTitle = 'Connexion';
|
||||
|
||||
// On ajoute ici le Header
|
||||
require_once('./../elements/header.php');
|
||||
@@ -16,8 +17,6 @@ if (isset($_SESSION['user_id'])){
|
||||
|
||||
?>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- CSS Login (fait par Intelligence Arti) -->
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
@@ -38,9 +37,7 @@ if (isset($_SESSION['user_id'])){
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
<?php
|
||||
|
||||
// Ajout du footer
|
||||
require_once('../elements/footer.php'); ?>
|
||||
require_once('../elements/footer.php'); ?>
|
||||
|
||||
+9
-4
@@ -1,20 +1,25 @@
|
||||
<?php
|
||||
<?php
|
||||
|
||||
// Page affichant les différents projets personnels / scolaires / professionnels
|
||||
// On pourrait également rajouter des tags comme "En cours" ou même "En reflexion"...
|
||||
|
||||
// Titre de l'onglet
|
||||
$pageTitle = 'Projets';
|
||||
|
||||
// On ajoute le header
|
||||
require_once('../elements/header.php');
|
||||
|
||||
// Ajout d'un message indiquant si l'utilisateur est admin qu'il peut modifier ces projets
|
||||
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
||||
echo "<p style='color: white; text-align: center;'>Vous êtes administrateur, vous pouvez modifier les projets <a href='../pages/admin.php'>ici</a>.</p>";
|
||||
echo "<p class='admin-notice'>Vous êtes administrateur, vous pouvez modifier les projets <a href='../pages/admin.php'>ici</a>.</p>";
|
||||
}
|
||||
// Affichages des projets
|
||||
?>
|
||||
|
||||
<p class="projects-title">Liste des projets</p>
|
||||
<div class="page-header">
|
||||
<h1>Projets</h1>
|
||||
<p>Mes réalisations personnelles, scolaires et professionnelles.</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
|
||||
@@ -25,4 +30,4 @@ require_once('../functions/listProject.php');
|
||||
afficherProjets();
|
||||
|
||||
// Ajout du footer
|
||||
require_once('../elements/footer.php'); ?>
|
||||
require_once('../elements/footer.php'); ?>
|
||||
|
||||
+20
-34
@@ -1,52 +1,38 @@
|
||||
<?php
|
||||
|
||||
// Compétences
|
||||
// Compétences : le contenu est maintenant dynamique, géré depuis le panel Admin (table skills)
|
||||
|
||||
// Titre de l'onglet
|
||||
$pageTitle = 'Compétences';
|
||||
|
||||
// On ajoute ici le header
|
||||
require_once('./../elements/header.php');
|
||||
|
||||
// Ajout d'un message indiquant si l'utilisateur est admin qu'il peut modifier ces compétences
|
||||
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
||||
echo "<p class='admin-notice'>Vous êtes administrateur, vous pouvez modifier les compétences <a href='../pages/admin.php'>ici</a>.</p>";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<!-- Contenu de la page skills -->
|
||||
<section class="skills-section">
|
||||
|
||||
<div class="container">
|
||||
<h1>Compétences</h1>
|
||||
</div>
|
||||
|
||||
<h2><i class="fa-solid fa-computer"></i> Systèmes d'exploitation :</h2>
|
||||
<ul>
|
||||
<li><i class="fa-brands fa-microsoft"></i> Windows (Server/XP 8 10 11)</li>
|
||||
<li><i class="fa-brands fa-linux"></i> Linux</li>
|
||||
<li><i class="fa-brands fa-apple"></i> macOS</li>
|
||||
</ul>
|
||||
|
||||
<h2>Réseaux :</h2>
|
||||
<ul>
|
||||
<li>Adressage IP</li>
|
||||
<li>VLAN</li>
|
||||
<li><i class="fa-brands fa-cloudflare"></i> Pare-feu</li>
|
||||
<li>DNS (Bind9)</li>
|
||||
<li>DHCP (ISC DHCP)</li>
|
||||
<li>Proxy (Proxy Squid)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Code :</h2>
|
||||
<ul>
|
||||
<li>HTML</li>
|
||||
<li>CSS</li>
|
||||
<li>JavaScript</li>
|
||||
<li>PHP</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="skills-header">
|
||||
<h1>Compétences</h1>
|
||||
<p>Un aperçu des technologies et des domaines que je maîtrise.</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once('../functions/listSkills.php');
|
||||
|
||||
// Appel de ma fonction dans Functions/listSkills.php
|
||||
afficherSkills();
|
||||
?>
|
||||
|
||||
</section>
|
||||
|
||||
<?php
|
||||
|
||||
// Ajout du footer
|
||||
require_once('../elements/footer.php'); ?>
|
||||
require_once('../elements/footer.php'); ?>
|
||||
|
||||
+66
-4
@@ -1,20 +1,25 @@
|
||||
<?php
|
||||
<?php
|
||||
|
||||
// Page affichant les différentes expériences pro (stages etc...)
|
||||
|
||||
// Titre de l'onglet
|
||||
$pageTitle = 'Expériences';
|
||||
|
||||
// On ajoute le header
|
||||
require_once('../elements/header.php');
|
||||
|
||||
// Ajout d'un message indiquant si l'utilisateur est admin qu'il peut modifier ces expériences
|
||||
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
||||
echo "<p style='color: white; text-align: center;'>Vous êtes administrateur, vous pouvez modifier les experiences <a href='../pages/admin.php'>ici</a>.</p>";
|
||||
echo "<p class='admin-notice'>Vous êtes administrateur, vous pouvez modifier les experiences <a href='../pages/admin.php'>ici</a>.</p>";
|
||||
}
|
||||
|
||||
// Affichages des xp pro
|
||||
?>
|
||||
|
||||
<p class="projects-title">Liste des expériences professionnelles</p>
|
||||
<div class="page-header" id="experiences">
|
||||
<h1>Expériences professionnelles</h1>
|
||||
<p>Mes stages et missions réalisées en entreprise.</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once('../functions/listXP.php');
|
||||
@@ -22,6 +27,63 @@ require_once('../functions/listXP.php');
|
||||
|
||||
// Appel de ma fonction dans Functions/listXP.php
|
||||
afficherXP();
|
||||
require_once('../functions/listSchool.php');
|
||||
|
||||
// Section écoles / formations, en dessous des expériences professionnelles
|
||||
// On n'affiche le titre que s'il y a au moins une école (table remplie)
|
||||
if (!empty(listeEcoles())) {
|
||||
?>
|
||||
|
||||
<div class="page-header" id="ecoles">
|
||||
<h2>Mes écoles</h2>
|
||||
<p>Mon parcours scolaire et mes formations.</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Appel de ma fonction dans Functions/listSchool.php
|
||||
afficherEcoles();
|
||||
?>
|
||||
|
||||
<!-- Bouton flottant : icône chapeau de diplômé = descendre vers les écoles,
|
||||
icône sac de travail = remonter vers les expériences -->
|
||||
<button class="section-jump" id="section-jump" aria-label="Basculer entre expériences et écoles">
|
||||
<i class="fa-solid fa-graduation-cap"></i>
|
||||
<i class="fa-solid fa-briefcase"></i>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var btn = document.getElementById('section-jump');
|
||||
var ecoles = document.getElementById('ecoles');
|
||||
if (!btn || !ecoles) return;
|
||||
|
||||
// Vrai quand la moitié de l'écran a dépassé le début de la section écoles
|
||||
function estDansEcoles() {
|
||||
return window.scrollY + window.innerHeight / 2 >= ecoles.offsetTop;
|
||||
}
|
||||
|
||||
// Change l'icône du bouton selon la section où on se trouve
|
||||
function majIcone() {
|
||||
btn.classList.toggle('mode-ecoles', estDansEcoles());
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (estDansEcoles()) {
|
||||
// On est dans les écoles : remonter aux expériences
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
// On est dans les expériences : descendre aux écoles
|
||||
ecoles.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', majIcone);
|
||||
majIcone();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
// Ajout du footer
|
||||
require_once('../elements/footer.php'); ?>
|
||||
require_once('../elements/footer.php'); ?>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Récupération des données dans la requete POST
|
||||
// La catégorie est restreinte à 'tool' ou 'partner' (sinon on retombe sur 'tool')
|
||||
$category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool';
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$icon = trim($_POST['icon'] ?? '');
|
||||
$logo_url = trim($_POST['logourl'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
$position = (int) ($_POST['position'] ?? 0);
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vides : il faut un nom, et
|
||||
// au moins l'un des deux visuels (icône Devicon ou image)
|
||||
if (!empty($name) && (!empty($icon) || !empty($logo_url))) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer le nouveau logo
|
||||
$stmt = $dbh->prepare('INSERT INTO logo_banners (category, name, icon, logo_url, link_url, position) VALUES (:category, :name, :icon, :logo_url, :link_url, :position)');
|
||||
|
||||
// On execute la requete préparée
|
||||
$stmt->execute([
|
||||
':category' => $category,
|
||||
':name' => $name,
|
||||
':icon' => $icon !== '' ? $icon : null,
|
||||
':logo_url' => $logo_url !== '' ? $logo_url : null,
|
||||
':link_url' => $link_url,
|
||||
':position' => $position
|
||||
]);
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout d'un logo (" . $category . ") : " . $name);
|
||||
|
||||
// Redirection vers home.php dès que la requete est executée
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// En cas d'erreur :
|
||||
echo "Erreur pendant l'ajout du logo: " . $e->getMessage();
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
echo "Veuillez remplir le nom et au moins une icône ou une image.";
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
// Si on y accède de façon peu légitime sans requête POST on refuse
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
+52
-10
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -8,29 +10,69 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Récupération des données dans la requete POST
|
||||
$title = $_POST['title'] ?? '';
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$tags = $_POST['tags'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$image_url = $_POST['imageurl'] ?? '';
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
// Le fichier envoyé n'est pas valide : on affiche l'erreur et on s'arrête
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
$pdfData = is_array($pdfResultat) ? $pdfResultat['data'] : null;
|
||||
$pdfFilename = is_array($pdfResultat) ? $pdfResultat['filename'] : null;
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vide
|
||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer le nouveau projet
|
||||
$stmt = $dbh->prepare('INSERT INTO projects (title, tags, description, image_url) VALUES (:title, :tags, :description, :image_url)');
|
||||
|
||||
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans project_pdfs (table
|
||||
// associée, voir migration Juillet 2026 (6)), pour que les listes de projets
|
||||
// n'aient jamais à charger son contenu binaire en mémoire
|
||||
$stmt = $dbh->prepare('INSERT INTO projects (title, status, tags, description, image_url, markdown, link_url) VALUES (:title, :status, :tags, :description, :image_url, :markdown, :link_url)');
|
||||
|
||||
// On execute la requete préparée
|
||||
$stmt->execute([
|
||||
':title' => $title,
|
||||
':tags' => $tags,
|
||||
':description' => $description,
|
||||
':image_url' => $image_url
|
||||
]);
|
||||
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
// Si un PDF a été envoyé, on l'enregistre à part dans project_pdfs
|
||||
if ($pdfData !== null) {
|
||||
$nouvelId = (int) $dbh->lastInsertId();
|
||||
$stmtPdf = $dbh->prepare('INSERT INTO project_pdfs (project_id, pdf_filename, pdf_data) VALUES (:project_id, :pdf_filename, :pdf_data)');
|
||||
$stmtPdf->bindValue(':project_id', $nouvelId, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout du projet : " . $title);
|
||||
|
||||
// Redirection vers projects.php ds que la requete est executée
|
||||
header('Location: ../pages/projects.php');
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Récupération des données dans la requete POST
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$duration = trim($_POST['duration'] ?? '');
|
||||
$image_url = trim($_POST['imageurl'] ?? '');
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
$pdfData = is_array($pdfResultat) ? $pdfResultat['data'] : null;
|
||||
$pdfFilename = is_array($pdfResultat) ? $pdfResultat['filename'] : null;
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vide
|
||||
if (!empty($title) && !empty($description) && !empty($duration) && !empty($image_url)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer la nouvelle école
|
||||
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans school_pdfs (table
|
||||
// associée, voir migration Juillet 2026 (6)), pour que les listes d'écoles
|
||||
// n'aient jamais à charger son contenu binaire en mémoire
|
||||
$stmt = $dbh->prepare('INSERT INTO schools (title, status, tags, description, duration, image_url, markdown, link_url) VALUES (:title, :status, :tags, :description, :duration, :image_url, :markdown, :link_url)');
|
||||
|
||||
// On execute la requete préparée
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
// Si un PDF a été envoyé, on l'enregistre à part dans school_pdfs
|
||||
if ($pdfData !== null) {
|
||||
$nouvelId = (int) $dbh->lastInsertId();
|
||||
$stmtPdf = $dbh->prepare('INSERT INTO school_pdfs (school_id, pdf_filename, pdf_data) VALUES (:school_id, :pdf_filename, :pdf_data)');
|
||||
$stmtPdf->bindValue(':school_id', $nouvelId, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout de l'école : " . $title);
|
||||
|
||||
// Redirection vers xp.php dès que la requete est executée
|
||||
header('Location: ../pages/xp.php');
|
||||
exit;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// En cas d'erreur :
|
||||
echo "Erreur pendant l'ajout de l'école: " . $e->getMessage();
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
echo "Veuillez remplir tous les champs obligatoires.";
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
// Si on y accède de façon peu légitime sans requête POST on refuse
|
||||
header('Location: ../pages/xp.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Récupération des données dans la requete POST
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$icon = trim($_POST['icon'] ?? '');
|
||||
$items = trim($_POST['items'] ?? '');
|
||||
$position = (int) ($_POST['position'] ?? 0);
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vide
|
||||
if (!empty($title) && !empty($items)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer la nouvelle catégorie de compétences
|
||||
$stmt = $dbh->prepare('INSERT INTO skills (title, icon, items, position) VALUES (:title, :icon, :items, :position)');
|
||||
|
||||
// On execute la requete préparée
|
||||
$stmt->execute([
|
||||
':title' => $title,
|
||||
':icon' => $icon,
|
||||
':items' => $items,
|
||||
':position' => $position
|
||||
]);
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout de la catégorie de compétences : " . $title);
|
||||
|
||||
// Redirection vers skills.php dès que la requete est executée
|
||||
header('Location: ../pages/skills.php');
|
||||
exit;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// En cas d'erreur :
|
||||
echo "Erreur pendant l'ajout de la compétence: " . $e->getMessage();
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
echo "Veuillez remplir tous les champs obligatoires.";
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
// Si on y accède de façon peu légitime sans requête POST on refuse
|
||||
header('Location: ../pages/skills.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
+9
-1
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// Ajout de connexion à la BDD
|
||||
require_once("../functions/databaseConnection.php");
|
||||
// Ajout des logs Discord
|
||||
require_once("../functions/sendLogs.php");
|
||||
|
||||
// Vérification que l'utilisateur est bien admin
|
||||
if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||
@@ -10,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||
exit();
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Si le formulaire a été soumis
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
// Récupération des données du formulaire
|
||||
@@ -58,6 +63,9 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
":created_at" => $dateCreation
|
||||
]);
|
||||
|
||||
// Log Discord
|
||||
sendLog("Création du compte : " . $user . " (rôle : " . $role . ")");
|
||||
|
||||
// Redirection vers la page admin après succès
|
||||
header("Location: ../pages/admin.php");
|
||||
exit();
|
||||
|
||||
+53
-12
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -8,33 +10,72 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Récupération des données dans la requete POST
|
||||
$title = $_POST['title'] ?? '';
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$xptype = $_POST['xptype'] ?? '';
|
||||
$tags = $_POST['tags'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$duration = $_POST['duration'] ?? '';
|
||||
$image_url = $_POST['imageurl'] ?? '';
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
$pdfData = is_array($pdfResultat) ? $pdfResultat['data'] : null;
|
||||
$pdfFilename = is_array($pdfResultat) ? $pdfResultat['filename'] : null;
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vide
|
||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer la nouvelle experience
|
||||
$stmt = $dbh->prepare('INSERT INTO experiences (title, xptype, tags, description, duration, image_url) VALUES (:title, :xptype, :tags, :description, :duration, :image_url)');
|
||||
|
||||
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans experience_pdfs (table
|
||||
// associée, voir migration Juillet 2026 (6)), pour que les listes d'expériences
|
||||
// n'aient jamais à charger son contenu binaire en mémoire
|
||||
$stmt = $dbh->prepare('INSERT INTO experiences (title, status, xptype, tags, description, duration, image_url, markdown, link_url) VALUES (:title, :status, :xptype, :tags, :description, :duration, :image_url, :markdown, :link_url)');
|
||||
|
||||
// On execute la requete préparée
|
||||
$stmt->execute([
|
||||
':title' => $title,
|
||||
':xptype' => $xptype,
|
||||
':tags' => $tags,
|
||||
':description' => $description,
|
||||
':duration' => $duration,
|
||||
':image_url' => $image_url
|
||||
]);
|
||||
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
// Si un PDF a été envoyé, on l'enregistre à part dans experience_pdfs
|
||||
if ($pdfData !== null) {
|
||||
$nouvelId = (int) $dbh->lastInsertId();
|
||||
$stmtPdf = $dbh->prepare('INSERT INTO experience_pdfs (experience_id, pdf_filename, pdf_data) VALUES (:experience_id, :pdf_filename, :pdf_data)');
|
||||
$stmtPdf->bindValue(':experience_id', $nouvelId, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout de l'expérience : " . $title);
|
||||
|
||||
// Redirection vers xp.php ds que la requete est executée
|
||||
header('Location: ../pages/xp.php');
|
||||
exit;
|
||||
|
||||
+16
-2
@@ -3,11 +3,14 @@
|
||||
// Verification de l'utilisateur voir si ces ids sont bon ou pas
|
||||
|
||||
// On démarre une session au cas où le script soit exécuté directement par URL
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Ajout des logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ./../pages/login.php');
|
||||
@@ -29,10 +32,17 @@ try {
|
||||
|
||||
// On regarde si le mdp correspond à celui en bdd
|
||||
if (!$user || !password_verify($password, $user['password'])) {
|
||||
// Log Discord : tentative de connexion échouée (utile pour repérer le bruteforce)
|
||||
sendLog("Tentative de connexion échouée avec l'identifiant : " . $username);
|
||||
header('Location: ./../pages/login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Régénère l'ID de session à la connexion : empêche la fixation de session
|
||||
// (un attaquant qui aurait imposé un ID de session à la victime avant sa
|
||||
// connexion ne peut plus récupérer une session authentifiée avec cet ID)
|
||||
session_regenerate_id(true);
|
||||
|
||||
// Si la connexion est ok, on entregistre l'utilisateur dans $_SESSION
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
@@ -46,7 +56,11 @@ try {
|
||||
error_log('Le serveur WEB ne peut pas modifier la date de dernière connexion : ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord : connexion réussie
|
||||
sendLog("Connexion réussie", $user['username']);
|
||||
|
||||
//Redirection vers home.php puisque que l'utilisateur est forcément connecté
|
||||
header('Location: ./../pages/home.php');
|
||||
// (?login=1 déclenche la notification "Connexion réussie" sur la page)
|
||||
header('Location: ./../pages/home.php?login=1');
|
||||
exit;
|
||||
?>
|
||||
+11
-3
@@ -2,8 +2,14 @@
|
||||
|
||||
// Lorsque l'utilisateur est renvoyé à cette page/fonction, il est déconnecté
|
||||
|
||||
// Démarrage de session
|
||||
session_start();
|
||||
// Démarrage de session (cookie durci)
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
|
||||
// Ajout des logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Log Discord AVANT la destruction de session (sinon on perd le nom d'utilisateur)
|
||||
sendLog("Déconnexion");
|
||||
|
||||
// On sauvegarde l'état du captcha avant destruction
|
||||
$captchaAlreadyValid = isset($_SESSION['captcha_valid']) && $_SESSION['captcha_valid'] === true;
|
||||
@@ -15,10 +21,12 @@ session_destroy();
|
||||
// Si le captcha avait déjà été validé, on recrée une session propre
|
||||
// pour conserver uniquement cette information
|
||||
if ($captchaAlreadyValid) {
|
||||
configureSecureSessionCookies();
|
||||
session_start();
|
||||
$_SESSION['captcha_valid'] = true;
|
||||
}
|
||||
|
||||
// Redirection de l'utilisateur vers l'accueil
|
||||
header('Location: ../index.php');
|
||||
// (?logout=1 déclenche la notification "Vous avez bien été déconnecté" sur la page)
|
||||
header('Location: ../pages/home.php?logout=1');
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
// Export de toutes les données du site en JSON (comme un dbdump mais en JSON)
|
||||
// Le fichier téléchargé peut ensuite être ré-importé via importData.php
|
||||
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Liste blanche des tables et colonnes exportées (empêche toute injection via noms de colonnes)
|
||||
// Depuis la migration Juillet 2026 (6), les PDF vivent dans des tables associées
|
||||
// (project_pdfs, experience_pdfs, school_pdfs) plutôt que dans projects/experiences/schools :
|
||||
// elles sont placées juste après leur table parente (l'ordre compte pour l'import, à cause
|
||||
// des clés étrangères ON DELETE CASCADE)
|
||||
$tables = [
|
||||
'settings' => ['name', 'value'],
|
||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
|
||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'experience_pdfs' => ['experience_id', 'pdf_filename', 'pdf_data'],
|
||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'school_pdfs' => ['school_id', 'pdf_filename', 'pdf_data'],
|
||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||
];
|
||||
|
||||
// Colonnes binaires : le JSON ne peut pas contenir de binaire brut, on les encode en base64
|
||||
// (+33% de taille mais nécessaire pour que le PDF survive à un export/import complet)
|
||||
$colonnesBinaires = [
|
||||
'project_pdfs' => ['pdf_data'],
|
||||
'experience_pdfs' => ['pdf_data'],
|
||||
'school_pdfs' => ['pdf_data'],
|
||||
];
|
||||
|
||||
// Métadonnées de l'export (pratique pour s'y retrouver dans les sauvegardes)
|
||||
$export = [
|
||||
'_meta' => [
|
||||
'site' => 'Portfolio Esteban MOUSSU',
|
||||
'date_export' => date('Y-m-d H:i:s'),
|
||||
'version' => 1
|
||||
]
|
||||
];
|
||||
|
||||
// Pour chaque table on récupère toutes les lignes
|
||||
foreach ($tables as $table => $colonnes) {
|
||||
try {
|
||||
// SELECT * puis filtrage par liste blanche : fonctionne même si certaines
|
||||
// colonnes optionnelles (markdown, link_url, pdf_data...) n'existent pas encore
|
||||
$stmt = $dbh->query('SELECT * FROM `' . $table . '`');
|
||||
$lignes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$binaires = $colonnesBinaires[$table] ?? [];
|
||||
|
||||
// On ne garde que les colonnes de la liste blanche, et on encode les colonnes binaires
|
||||
$export[$table] = array_map(function ($ligne) use ($colonnes, $binaires) {
|
||||
$ligne = array_intersect_key($ligne, array_flip($colonnes));
|
||||
foreach ($binaires as $colonne) {
|
||||
if (isset($ligne[$colonne]) && $ligne[$colonne] !== null) {
|
||||
$ligne[$colonne] = base64_encode($ligne[$colonne]);
|
||||
}
|
||||
}
|
||||
return $ligne;
|
||||
}, $lignes);
|
||||
} catch (PDOException $e) {
|
||||
// Si une table n'existe pas encore on l'exporte vide plutôt que de bloquer l'export
|
||||
$export[$table] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Export des données du site en JSON");
|
||||
|
||||
// Envoi du fichier JSON en téléchargement
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="portfolio-backup-' . date('Y-m-d-His') . '.json"');
|
||||
echo json_encode($export, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
// Import d'une sauvegarde JSON (créée par exportData.php)
|
||||
// ATTENTION : les données actuelles des tables présentes dans le fichier sont ÉCRASÉES
|
||||
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// La requête doit être un POST avec un fichier sinon on refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['jsonfile']) || $_FILES['jsonfile']['error'] !== UPLOAD_ERR_OK) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Lecture et décodage du fichier JSON
|
||||
$contenu = file_get_contents($_FILES['jsonfile']['tmp_name']);
|
||||
$donnees = json_decode($contenu, true);
|
||||
|
||||
if (!is_array($donnees)) {
|
||||
die("Erreur : le fichier n'est pas un JSON valide.");
|
||||
}
|
||||
|
||||
// Même liste blanche que l'export : seules ces tables / colonnes peuvent être importées.
|
||||
// L'ORDRE COMPTE : chaque table de PDF associée (project_pdfs...) doit être importée
|
||||
// juste après sa table parente (projects...), car sa clé étrangère exige que la ligne
|
||||
// parente existe déjà (voir migration Juillet 2026 (6))
|
||||
$tables = [
|
||||
'settings' => ['name', 'value'],
|
||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
|
||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'experience_pdfs' => ['experience_id', 'pdf_filename', 'pdf_data'],
|
||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'school_pdfs' => ['school_id', 'pdf_filename', 'pdf_data'],
|
||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||
];
|
||||
|
||||
// Colonnes binaires : encodées en base64 par exportData.php, à redécoder ici
|
||||
$colonnesBinaires = [
|
||||
'project_pdfs' => ['pdf_data'],
|
||||
'experience_pdfs' => ['pdf_data'],
|
||||
'school_pdfs' => ['pdf_data'],
|
||||
];
|
||||
|
||||
try {
|
||||
// Transaction : si une erreur survient, TOUT est annulé (la base reste intacte)
|
||||
$dbh->beginTransaction();
|
||||
|
||||
foreach ($tables as $table => $colonnesAutorisees) {
|
||||
// On ne touche qu'aux tables présentes dans le fichier
|
||||
if (!isset($donnees[$table]) || !is_array($donnees[$table])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sécurité : on refuse de vider la table users si la sauvegarde n'en contient aucun
|
||||
// (sinon l'admin se retrouverait enfermé dehors sans aucun compte)
|
||||
if ($table === 'users' && count($donnees[$table]) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// On écrase les données existantes de la table
|
||||
$dbh->exec('DELETE FROM `' . $table . '`');
|
||||
|
||||
// Colonnes binaires (PDF) de cette table, à redécoder du base64
|
||||
$binaires = $colonnesBinaires[$table] ?? [];
|
||||
|
||||
// Puis on insère chaque ligne de la sauvegarde
|
||||
foreach ($donnees[$table] as $ligne) {
|
||||
if (!is_array($ligne)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// On ne garde que les colonnes autorisées ET présentes dans la ligne
|
||||
$colonnes = array_values(array_intersect($colonnesAutorisees, array_keys($ligne)));
|
||||
if (empty($colonnes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$placeholders = array_map(function ($c) { return ':' . $c; }, $colonnes);
|
||||
$stmt = $dbh->prepare(
|
||||
'INSERT INTO `' . $table . '` (`' . implode('`, `', $colonnes) . '`) VALUES (' . implode(', ', $placeholders) . ')'
|
||||
);
|
||||
|
||||
foreach ($colonnes as $colonne) {
|
||||
$valeur = $ligne[$colonne];
|
||||
|
||||
// Les colonnes binaires (pdf_data) sont ré-encodées en base64 par l'export : on les redécode
|
||||
if (in_array($colonne, $binaires, true) && $valeur !== null) {
|
||||
$stmt->bindValue(':' . $colonne, base64_decode($valeur), PDO::PARAM_LOB);
|
||||
} else {
|
||||
$stmt->bindValue(':' . $colonne, $valeur);
|
||||
}
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
||||
|
||||
// Tout s'est bien passé : on valide définitivement
|
||||
$dbh->commit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
// En cas d'erreur on annule tout : la base revient à son état d'avant l'import
|
||||
$dbh->rollBack();
|
||||
die("Erreur pendant l'import, aucune donnée n'a été modifiée : " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Import d'une sauvegarde JSON : données du site écrasées");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php (?import=1 déclenche la notification de succès)
|
||||
header('Location: ../pages/admin.php?import=1');
|
||||
exit;
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// On collecte l'id et on le met en variable locale
|
||||
$id = (int) $_GET['id'];
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('DELETE FROM logo_banners WHERE id = :id');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression du logo :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression du logo id " . $id);
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php, directement sur la section Logos
|
||||
header('Location: ../pages/admin.php#admin-logos');
|
||||
exit;
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -10,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
@@ -26,6 +31,9 @@ try { // On essaie d'executer une requête qui est préparée
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression du projet :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression du projet id " . $id);
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// On collecte l'id et on le met en variable locale
|
||||
$id = (int) $_GET['id'];
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('DELETE FROM schools WHERE id = :id');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression de l'école :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression de l'école id " . $id);
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// On collecte l'id et on le met en variable locale
|
||||
$id = (int) $_GET['id'];
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('DELETE FROM skills WHERE id = :id');
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression de la compétence :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression de la catégorie de compétences id " . $id);
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// Ajout de connexion à la BDD
|
||||
require_once("../functions/databaseConnection.php");
|
||||
// Ajout des logs Discord
|
||||
require_once("../functions/sendLogs.php");
|
||||
|
||||
// Vérification que l'utilisateur est bien admin
|
||||
if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||
@@ -10,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||
exit();
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Récupération de l'ID passé en paramètre GET
|
||||
$id = $_GET["id"];
|
||||
|
||||
@@ -26,6 +31,9 @@ if ($id == "") {
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression du compte id " . $id);
|
||||
|
||||
// Redirection vers la page admin après suppression
|
||||
header("Location: ../pages/admin.php");
|
||||
exit();
|
||||
|
||||
+10
-1
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -10,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
@@ -26,6 +31,10 @@ try { // On essaie d'executer une requête qui est préparée
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression de l experience :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression de l'expérience id " . $id);
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -1,6 +1,11 @@
|
||||
<?php
|
||||
|
||||
if (!isset($_SESSION['captcha_valid']) || $_SESSION['captcha_valid'] !== true) {
|
||||
require_once(__DIR__ . '/../functions/listSettings.php');
|
||||
|
||||
// Le captcha peut être désactivé depuis le panel Admin (section Sécurité)
|
||||
$captchaEnabled = recupererSetting('captcha_enabled') ?? '1';
|
||||
|
||||
if ($captchaEnabled === '1' && (!isset($_SESSION['captcha_valid']) || $_SESSION['captcha_valid'] !== true)) {
|
||||
header('Location: ../captcha.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// Envoi de message de contact via WebHook discord => Correction de faille de sécurité en cachant le webhook dans un fichier à part
|
||||
require_once('../../idDiscord.php');
|
||||
|
||||
// Ajout des logs Discord
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Envoi de formulaire de contact via WebHook discord
|
||||
$webhook = ID_DISCORD2; // URL du webhook Discord
|
||||
// EXEMPLE de a quoi ressemble idDiscord :
|
||||
@@ -55,12 +59,13 @@ if ($subject == "" || $name == "" || $mail == "" || $message == "") {
|
||||
$resultat = curl_exec($ch);
|
||||
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
// (pas de curl_close : déprécié depuis PHP 8.5, la ressource se libère toute seule)
|
||||
if ($codeHttp == 204) { // Si ça renvoi 204 alors le message a été correctement envoyé
|
||||
curl_close($ch); // Fermer la ressource curl lancée
|
||||
// Log Discord : message de contact envoyé
|
||||
sendLog("Nouveau message de contact envoyé par : " . $name);
|
||||
header("Location: ../pages/contact.php?success=1");
|
||||
exit();
|
||||
} else { // Sinon c'est que ça n'a pas été envoyé (discord down / webhook expiré / etcc...)
|
||||
curl_close($ch); // Fermer la ressource curl lancée
|
||||
echo "Le message n'a pas pu être envoyé sur Discord.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
empty($_POST['name']) ||
|
||||
(empty($_POST['icon']) && empty($_POST['logourl']))
|
||||
) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// On récupère les données pour les mettres en variables locales
|
||||
$id = (int) $_POST['id'];
|
||||
$category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool';
|
||||
$name = trim($_POST['name']);
|
||||
$icon = trim($_POST['icon'] ?? '');
|
||||
$logourl = trim($_POST['logourl'] ?? '');
|
||||
$linkurl = trim($_POST['linkurl'] ?? '');
|
||||
$position = (int) ($_POST['position'] ?? 0);
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('
|
||||
UPDATE logo_banners
|
||||
SET category = :category, name = :name, icon = :icon, logo_url = :logo_url, link_url = :link_url, position = :position
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':category', $category, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':name', $name, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':icon', $icon !== '' ? $icon : null, $icon !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||
$stmt->bindValue(':logo_url', $logourl !== '' ? $logourl : null, $logourl !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||
$stmt->bindValue(':link_url', $linkurl, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':position', $position, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification du logo :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification du logo : " . $name . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php, directement sur la section Logos
|
||||
header('Location: ../pages/admin.php#admin-logos');
|
||||
exit;
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -10,6 +13,15 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
empty($_POST['title']) ||
|
||||
@@ -23,25 +35,77 @@ if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
// On récupère les données pour les mettres en variables locales
|
||||
$id = (int) $_POST['id'];
|
||||
$title = trim($_POST['title']);
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$description = trim($_POST['description']);
|
||||
$imageurl = trim($_POST['imageurl']);
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
// Le fichier envoyé n'est pas valide : on affiche l'erreur et on s'arrête
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le PDF n'est mis à jour que si un nouveau fichier est envoyé OU si la case
|
||||
// "Supprimer ce PDF" est cochée. Sinon on garde le PDF actuel intact.
|
||||
$updatePdf = false;
|
||||
$pdfData = null;
|
||||
$pdfFilename = null;
|
||||
if (is_array($pdfResultat)) {
|
||||
$updatePdf = true;
|
||||
$pdfData = $pdfResultat['data'];
|
||||
$pdfFilename = $pdfResultat['filename'];
|
||||
} elseif (isset($_POST['remove_pdf'])) {
|
||||
$updatePdf = true;
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
// Le PDF (pdf_data) n'est plus stocké dans projects : il vit dans project_pdfs (table
|
||||
// associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||
$stmt = $dbh->prepare('
|
||||
UPDATE projects
|
||||
SET title = :title, tags = :tags, description = :description, image_url = :image_url
|
||||
SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
|
||||
if ($updatePdf) {
|
||||
if ($pdfData !== null) {
|
||||
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = project_id)
|
||||
$stmtPdf = $dbh->prepare('
|
||||
INSERT INTO project_pdfs (project_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||
');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
} else {
|
||||
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||
$stmtPdf = $dbh->prepare('DELETE FROM project_pdfs WHERE project_id = :id');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification du projet :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification du projet : " . $title . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
empty($_POST['title']) ||
|
||||
empty($_POST['description']) ||
|
||||
empty($_POST['duration']) ||
|
||||
empty($_POST['imageurl'])
|
||||
) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// On récupère les données pour les mettres en variables locales
|
||||
$id = (int) $_POST['id'];
|
||||
$title = trim($_POST['title']);
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$description = trim($_POST['description']);
|
||||
$duration = trim($_POST['duration']);
|
||||
$imageurl = trim($_POST['imageurl']);
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le PDF n'est mis à jour que si un nouveau fichier est envoyé OU si la case
|
||||
// "Supprimer ce PDF" est cochée. Sinon on garde le PDF actuel intact.
|
||||
$updatePdf = false;
|
||||
$pdfData = null;
|
||||
$pdfFilename = null;
|
||||
if (is_array($pdfResultat)) {
|
||||
$updatePdf = true;
|
||||
$pdfData = $pdfResultat['data'];
|
||||
$pdfFilename = $pdfResultat['filename'];
|
||||
} elseif (isset($_POST['remove_pdf'])) {
|
||||
$updatePdf = true;
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
// Le PDF (pdf_data) n'est plus stocké dans schools : il vit dans school_pdfs (table
|
||||
// associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||
$stmt = $dbh->prepare('
|
||||
UPDATE schools
|
||||
SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
|
||||
if ($updatePdf) {
|
||||
if ($pdfData !== null) {
|
||||
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = school_id)
|
||||
$stmtPdf = $dbh->prepare('
|
||||
INSERT INTO school_pdfs (school_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||
');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
} else {
|
||||
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||
$stmtPdf = $dbh->prepare('DELETE FROM school_pdfs WHERE school_id = :id');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de l'école :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification de l'école : " . $title . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// On récupère les données pour les mettres en variables locales
|
||||
// La checkbox n'est envoyée que si elle est cochée
|
||||
// settings_group : évite qu'un formulaire écrase les réglages d'un autre (ex : la case
|
||||
// "notice_enabled" non cochée ne doit pas désactiver le captcha, et inversement)
|
||||
$groupeReglages = $_POST['settings_group'] ?? 'notice';
|
||||
|
||||
if ($groupeReglages === 'captcha') {
|
||||
$reglages = [
|
||||
'captcha_enabled' => isset($_POST['captcha_enabled']) ? '1' : '0'
|
||||
];
|
||||
} elseif ($groupeReglages === 'contact') {
|
||||
$reglages = [
|
||||
'contact_email' => trim($_POST['contact_email'] ?? ''),
|
||||
'contact_email_enabled' => isset($_POST['contact_email_enabled']) ? '1' : '0',
|
||||
'contact_address_line1' => trim($_POST['contact_address_line1'] ?? ''),
|
||||
'contact_address_line2' => trim($_POST['contact_address_line2'] ?? ''),
|
||||
'contact_address_enabled' => isset($_POST['contact_address_enabled']) ? '1' : '0'
|
||||
];
|
||||
} else {
|
||||
$reglages = [
|
||||
'site_notice_title' => trim($_POST['notice_title'] ?? ''),
|
||||
'site_notice_text' => trim($_POST['notice_text'] ?? ''),
|
||||
'site_notice_enabled' => isset($_POST['notice_enabled']) ? '1' : '0'
|
||||
];
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée pour chaque réglage
|
||||
// ON DUPLICATE KEY UPDATE : crée le réglage s'il n'existe pas encore, le met à jour sinon
|
||||
$stmt = $dbh->prepare('INSERT INTO settings (name, value) VALUES (:name, :value) ON DUPLICATE KEY UPDATE value = VALUES(value)');
|
||||
|
||||
foreach ($reglages as $name => $value) {
|
||||
$stmt->execute([
|
||||
':name' => $name,
|
||||
':value' => $value
|
||||
]);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de la notification :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
$libellesGroupes = [
|
||||
'captcha' => 'Modification du réglage du captcha',
|
||||
'contact' => 'Modification des coordonnées de contact',
|
||||
'notice' => "Modification de la notification de la page d'accueil"
|
||||
];
|
||||
sendLog($libellesGroupes[$groupeReglages] ?? "Modification des réglages du site");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
empty($_POST['title']) ||
|
||||
empty($_POST['items'])
|
||||
) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// On récupère les données pour les mettres en variables locales
|
||||
$id = (int) $_POST['id'];
|
||||
$title = trim($_POST['title']);
|
||||
$icon = trim($_POST['icon'] ?? '');
|
||||
$items = trim($_POST['items']);
|
||||
$position = (int) ($_POST['position'] ?? 0);
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('
|
||||
UPDATE skills
|
||||
SET title = :title, icon = :icon, items = :items, position = :position
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':icon', $icon, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':items', $items, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':position', $position, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de la compétence :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification de la catégorie de compétences : " . $title . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
+65
-2
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -10,6 +13,15 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
// Si le formulaire à un champ vide
|
||||
if (
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
@@ -26,29 +38,80 @@ if (
|
||||
//On récupère les données pour les mettres en variables locales
|
||||
$id = (int) $_POST['id'];
|
||||
$title = trim($_POST['title']);
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$xptype = trim($_POST['xptype']);
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$description = trim($_POST['description']);
|
||||
$duration = trim($_POST['duration']);
|
||||
$imageurl = trim($_POST['imageurl']);
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le PDF n'est mis à jour que si un nouveau fichier est envoyé OU si la case
|
||||
// "Supprimer ce PDF" est cochée. Sinon on garde le PDF actuel intact.
|
||||
$updatePdf = false;
|
||||
$pdfData = null;
|
||||
$pdfFilename = null;
|
||||
if (is_array($pdfResultat)) {
|
||||
$updatePdf = true;
|
||||
$pdfData = $pdfResultat['data'];
|
||||
$pdfFilename = $pdfResultat['filename'];
|
||||
} elseif (isset($_POST['remove_pdf'])) {
|
||||
$updatePdf = true;
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
// Le PDF (pdf_data) n'est plus stocké dans experiences : il vit dans experience_pdfs
|
||||
// (table associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||
$stmt = $dbh->prepare('
|
||||
UPDATE experiences
|
||||
SET title = :title, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url
|
||||
SET title = :title, status = :status, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
|
||||
if ($updatePdf) {
|
||||
if ($pdfData !== null) {
|
||||
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = experience_id)
|
||||
$stmtPdf = $dbh->prepare('
|
||||
INSERT INTO experience_pdfs (experience_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||
');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
} else {
|
||||
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||
$stmtPdf = $dbh->prepare('DELETE FROM experience_pdfs WHERE experience_id = :id');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de l experience :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification de l'expérience : " . $title . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
// Sert le PDF stocké en base pour un projet / une expérience / une école
|
||||
// Utilisé en <iframe> par la page de détails (mini lecteur) et pour le téléchargement direct
|
||||
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Liste blanche des types autorisés : empêche toute injection de nom de table
|
||||
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6),
|
||||
// pour ne plus jamais charger son contenu binaire lors des listes de projets/xp/écoles)
|
||||
$tables = [
|
||||
'projet' => ['pdf' => 'project_pdfs', 'colonne' => 'project_id'],
|
||||
'experience' => ['pdf' => 'experience_pdfs', 'colonne' => 'experience_id'],
|
||||
'ecole' => ['pdf' => 'school_pdfs', 'colonne' => 'school_id'],
|
||||
];
|
||||
|
||||
$type = $_GET['type'] ?? '';
|
||||
$id = $_GET['id'] ?? '';
|
||||
|
||||
if (!isset($tables[$type]) || !is_numeric($id)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('SELECT pdf_data, pdf_filename FROM `' . $tables[$type]['pdf'] . '` WHERE `' . $tables[$type]['colonne'] . '` = :id');
|
||||
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$row || $row['pdf_data'] === null) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$filename = !empty($row['pdf_filename']) ? $row['pdf_filename'] : 'document.pdf';
|
||||
|
||||
// ?download=1 force le téléchargement, sinon le PDF s'affiche dans le mini lecteur (iframe)
|
||||
$disposition = (isset($_GET['download']) && $_GET['download'] === '1') ? 'attachment' : 'inline';
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: ' . $disposition . '; filename="' . basename($filename) . '"');
|
||||
header('Content-Length: ' . strlen($row['pdf_data']));
|
||||
header('Cache-Control: private, max-age=3600');
|
||||
echo $row['pdf_data'];
|
||||
exit;
|
||||
+203
-33
@@ -1,9 +1,9 @@
|
||||
body {
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(108, 99, 255, 0.18), transparent 35%),
|
||||
#0d0d0d;
|
||||
radial-gradient(circle at top, rgba(255, 122, 26, 0.16), transparent 35%),
|
||||
#0a0a10;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
@@ -21,10 +21,22 @@ body {
|
||||
.captcha-card,
|
||||
.success-screen {
|
||||
background: linear-gradient(180deg, #111116 0%, #161622 100%);
|
||||
border: 1px solid #2a2a3e;
|
||||
border: 1px solid #262638;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
|
||||
padding: 30px;
|
||||
animation: cardEnter 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes cardEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-badge {
|
||||
@@ -33,9 +45,9 @@ body {
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(108, 99, 255, 0.14);
|
||||
border: 1px solid rgba(108, 99, 255, 0.28);
|
||||
color: #9d97ff;
|
||||
background: rgba(255, 122, 26, 0.14);
|
||||
border: 1px solid rgba(255, 122, 26, 0.3);
|
||||
color: #ffa155;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 18px;
|
||||
@@ -63,10 +75,14 @@ body {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.captcha-subtitle strong {
|
||||
color: #ffa155;
|
||||
}
|
||||
|
||||
.captcha-target {
|
||||
min-width: 240px;
|
||||
background-color: #1a1a24;
|
||||
border: 1px solid #2a2a3e;
|
||||
border: 1px solid #262638;
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
}
|
||||
@@ -82,7 +98,7 @@ body {
|
||||
|
||||
.captcha-target strong {
|
||||
font-size: 22px;
|
||||
color: #8f88ff;
|
||||
color: #ffa155;
|
||||
}
|
||||
|
||||
.captcha-stats {
|
||||
@@ -95,6 +111,9 @@ body {
|
||||
}
|
||||
|
||||
.captcha-attempts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
background-color: #171b28;
|
||||
border: 1px solid #283049;
|
||||
border-radius: 14px;
|
||||
@@ -115,6 +134,28 @@ body {
|
||||
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-secondary-button {
|
||||
display: inline-flex;
|
||||
@@ -124,14 +165,15 @@ body {
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
background-color: #2a314a;
|
||||
border: 1px solid #3d4767;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
background-color: #2a2a3a;
|
||||
border: 1px solid #3a3a4e;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.captcha-reset:hover,
|
||||
.captcha-secondary-button:hover {
|
||||
background-color: #394362;
|
||||
background-color: #34344a;
|
||||
border-color: rgba(255, 122, 26, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -143,6 +185,15 @@ body {
|
||||
color: #ffb8bb;
|
||||
padding: 14px 16px;
|
||||
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 {
|
||||
@@ -199,10 +250,25 @@ body {
|
||||
|
||||
.mail-option {
|
||||
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 {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.mail-option input {
|
||||
@@ -211,9 +277,15 @@ body {
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(120px, 220px) 1fr 70px;
|
||||
grid-template-columns: 26px 48px minmax(120px, 220px) 1fr 70px;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
padding: 16px 18px;
|
||||
@@ -232,16 +304,38 @@ body {
|
||||
}
|
||||
|
||||
.mail-option input:checked + .mail-row {
|
||||
background-color: rgba(108, 99, 255, 0.14);
|
||||
box-shadow: inset 0 0 0 1px rgba(108, 99, 255, 0.35);
|
||||
background-color: rgba(255, 122, 26, 0.12);
|
||||
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 {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #2f3550 0%, #1f2334 100%);
|
||||
border: 1px solid #303650;
|
||||
background: linear-gradient(180deg, #2a2a3a 0%, #1a1a24 100%);
|
||||
border: 1px solid #303040;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -287,6 +381,16 @@ body {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -308,7 +412,7 @@ body {
|
||||
}
|
||||
|
||||
.captcha-submit {
|
||||
background: linear-gradient(180deg, #7a72ff 0%, #5e56eb 100%);
|
||||
background: linear-gradient(180deg, #ff8c3a 0%, #e8630a 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
@@ -317,12 +421,16 @@ body {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
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 {
|
||||
@@ -348,12 +456,60 @@ body {
|
||||
}
|
||||
|
||||
.success-screen p {
|
||||
margin: 0 auto 24px;
|
||||
margin: 0 auto 12px;
|
||||
max-width: 520px;
|
||||
color: #ced2e6;
|
||||
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 {
|
||||
width: 240px;
|
||||
height: 10px;
|
||||
@@ -368,8 +524,8 @@ body {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: linear-gradient(90deg, #62d26f 0%, #9cffaf 100%);
|
||||
animation: loadingBar 1.8s linear forwards;
|
||||
background: linear-gradient(90deg, #ffa155 0%, #ff7a1a 100%);
|
||||
animation: loadingBar 2.4s linear forwards;
|
||||
}
|
||||
|
||||
.success-burst {
|
||||
@@ -377,10 +533,10 @@ body {
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 215, 0, 0.25), transparent 18%),
|
||||
radial-gradient(circle at 80% 20%, rgba(98, 210, 111, 0.22), transparent 18%),
|
||||
radial-gradient(circle at 30% 80%, rgba(122, 114, 255, 0.2), transparent 20%),
|
||||
radial-gradient(circle at 75% 70%, rgba(255, 105, 180, 0.16), 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.2), transparent 18%),
|
||||
radial-gradient(circle at 30% 80%, rgba(255, 122, 26, 0.22), transparent 20%),
|
||||
radial-gradient(circle at 75% 70%, rgba(255, 145, 77, 0.18), transparent 18%);
|
||||
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) {
|
||||
.captcha-top {
|
||||
flex-direction: column;
|
||||
@@ -440,7 +612,7 @@ body {
|
||||
}
|
||||
|
||||
.mail-row {
|
||||
grid-template-columns: 42px 1fr 56px;
|
||||
grid-template-columns: 22px 42px 1fr 56px;
|
||||
}
|
||||
|
||||
.mail-sender {
|
||||
@@ -482,15 +654,13 @@ body {
|
||||
}
|
||||
|
||||
.mail-row {
|
||||
grid-template-columns: 36px 1fr;
|
||||
grid-template-columns: 20px 36px 1fr;
|
||||
gap: 10px;
|
||||
padding: 14px 14px;
|
||||
}
|
||||
|
||||
.mail-time {
|
||||
grid-column: 2;
|
||||
text-align: left;
|
||||
margin-top: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.success-screen h1 {
|
||||
|
||||
+1891
-322
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user