This commit is contained in:
Esteban
2026-07-03 02:02:13 +02:00
parent 88c10ee5b0
commit a19c5c8381
42 changed files with 578 additions and 129 deletions
+9
View File
@@ -0,0 +1,9 @@
# Config Claude Code (local, pas utile au dépôt)
.claude/
# macOS
.DS_Store
# Éditeurs
.vscode/
.idea/
+1 -1
View File
@@ -1,5 +1,5 @@
<?php <?php
session_start(); require_once(__DIR__ . '/functions/security.php');
// Ajout des logs Discord (chemin via __DIR__ car captcha.php est à la racine du site) // Ajout des logs Discord (chemin via __DIR__ car captcha.php est à la racine du site)
require_once(__DIR__ . '/functions/sendLogs.php'); require_once(__DIR__ . '/functions/sendLogs.php');
+61
View File
@@ -180,3 +180,64 @@ ADD COLUMN pdf_filename VARCHAR(255) NULL;
ALTER TABLE logo_banners ALTER TABLE logo_banners
ADD COLUMN icon VARCHAR(100) NULL AFTER name, ADD COLUMN icon VARCHAR(100) NULL AFTER name,
MODIFY COLUMN logo_url VARCHAR(255) NULL; MODIFY COLUMN logo_url VARCHAR(255) NULL;
-- ============================================================
-- Juillet 2026 (6) : sortie des PDF des tables projects/experiences/schools
-- vers des tables associées dédiées, pour que les pages qui LISTENT ces
-- éléments (admin.php, xp.php, projects.php...) n'aient plus à charger les
-- PDF (jusqu'à 15 Mo chacun) en mémoire à chaque affichage. C'était la
-- cause du ralentissement d'admin.php et de xp.php : ces pages faisaient
-- un SELECT * qui remontait aussi tout le contenu binaire des PDF alors
-- que seul le nom de fichier est utilisé dans les listes.
--
-- ÉTAPE 1/2 (non destructive) : crée les nouvelles tables et copie les PDF
-- existants dedans. Les colonnes pdf_data / pdf_filename des tables
-- d'origine ne sont PAS touchées à cette étape : aucune perte de donnée
-- possible, elles restent en place comme copie de sécurité.
-- ============================================================
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
);
-- Copie des PDF déjà en base vers les nouvelles tables (ne copie que les lignes qui ont
-- effectivement un PDF ; ré-exécutable sans doublon grâce à la clé primaire = id du parent)
INSERT IGNORE INTO project_pdfs (project_id, pdf_filename, pdf_data)
SELECT id, pdf_filename, pdf_data FROM projects WHERE pdf_data IS NOT NULL;
INSERT IGNORE INTO experience_pdfs (experience_id, pdf_filename, pdf_data)
SELECT id, pdf_filename, pdf_data FROM experiences WHERE pdf_data IS NOT NULL;
INSERT IGNORE INTO school_pdfs (school_id, pdf_filename, pdf_data)
SELECT id, pdf_filename, pdf_data FROM schools WHERE pdf_data IS NOT NULL;
-- ============================================================
-- ÉTAPE 2/2 (destructive, À NE PAS EXÉCUTER MAINTENANT) : à lancer plus
-- tard, une fois que le site a été vérifié (listes, modification, suppression,
-- lecteur PDF) et qu'on est sûr à 100% que tout fonctionne avec les
-- nouvelles tables. Supprime les anciennes colonnes devenues inutiles.
-- ============================================================
-- ALTER TABLE projects DROP COLUMN pdf_data, DROP COLUMN pdf_filename;
-- ALTER TABLE experiences DROP COLUMN pdf_data, DROP COLUMN pdf_filename;
-- ALTER TABLE schools DROP COLUMN pdf_data, DROP COLUMN pdf_filename;
+2 -1
View File
@@ -52,7 +52,8 @@ if (isset($_GET['success']) && $_GET['success'] === '1') {
var cibles = document.querySelectorAll( var cibles = document.querySelectorAll(
'.project-card, .quick-link-card, .skill-category, .contact-card, ' + '.project-card, .quick-link-card, .skill-category, .contact-card, ' +
'.login-card, .project-form-card, .cgu-card, .logo-admin-item, ' + '.login-card, .project-form-card, .cgu-card, .logo-admin-item, ' +
'.page-header, .quick-links-header, .skills-header, .projects-title, .logo-banner-header' '.page-header, .quick-links-header, .skills-header, .projects-title, .logo-banner-header, ' +
'.stat-item'
); );
var observer = new IntersectionObserver(function (entries) { var observer = new IntersectionObserver(function (entries) {
+26 -1
View File
@@ -6,7 +6,7 @@
// Ajout ici d'un démarrage de session : header étant dans toutes les pages // 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 // Ajout ici la vérification du captcha
require_once('../scripts/requireCaptcha.php'); require_once('../scripts/requireCaptcha.php');
@@ -20,6 +20,15 @@ sendPageLog();
// Titre de la page : chaque page peut définir $pageTitle avant d'inclure le header // Titre de la page : chaque page peut définir $pageTitle avant d'inclure le header
$pageTitle = isset($pageTitle) ? $pageTitle . ' — Esteban MOUSSU' : 'Esteban MOUSSU — Portfolio'; $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 // Page courante pour souligner le lien actif dans la navigation
$currentPage = basename($_SERVER['PHP_SELF']); $currentPage = basename($_SERVER['PHP_SELF']);
?> ?>
@@ -31,6 +40,22 @@ $currentPage = basename($_SERVER['PHP_SELF']);
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($pageTitle); ?></title> <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 --> <!-- Favicon du site -->
<link rel="icon" type="image/png" href="../memoji.png"> <link rel="icon" type="image/png" href="../memoji.png">
+1 -1
View File
@@ -118,7 +118,7 @@ function afficherLogosAdmin() {
echo '</div>'; echo '</div>';
echo '<div class="admin-actions">'; 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-edit" href="./admin.php?edit_logo=' . $id . '#admin-logos">Modifier</a>';
echo '<a class="btn-submit btn-delete" href="../scripts/removeLogo.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce logo ?\');">Supprimer</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>'; echo '</div>';
} }
+21 -6
View File
@@ -10,10 +10,17 @@ function listeProjets() {
// Essayer // Essayer
try { try {
// Requête SQL qui récupère titre, tags, description, image_url // On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne // stocké à part dans project_pdfs (voir migration Juillet 2026 (6)), on ne récupère
// même si la migration SQL n'a pas encore été exécutée) // ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
$stmt = $dbh->prepare('SELECT * FROM projects ORDER BY id DESC'); // (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 // Execution de la requête SQL
$stmt->execute(); $stmt->execute();
// Retourne toutes les données collectés dans un ARRAY // Retourne toutes les données collectés dans un ARRAY
@@ -29,7 +36,15 @@ function recupererProjetParId($id) {
global $dbh; global $dbh;
try { try {
$stmt = $dbh->prepare('SELECT * 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->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
@@ -164,7 +179,7 @@ function afficherProjetsAdmin() {
// Boutons qui vont permettrent la modification / Suppresion // Boutons qui vont permettrent la modification / Suppresion
echo '<div class="admin-actions">'; echo '<div class="admin-actions">';
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '#admin-projets">Modifier</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 . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce projet ?\');">Supprimer</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>';
echo '</div>'; echo '</div>';
+21 -5
View File
@@ -10,9 +10,17 @@ function listeEcoles() {
global $dbh; global $dbh;
try { try {
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne // On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
// même si la migration SQL n'a pas encore été exécutée) // stocké à part dans school_pdfs (voir migration Juillet 2026 (6)), on ne récupère
$stmt = $dbh->prepare('SELECT * FROM schools ORDER BY id DESC'); // 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(); $stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) { } catch (PDOException $e) {
@@ -25,7 +33,15 @@ function recupererEcoleParId($id) {
global $dbh; global $dbh;
try { try {
$stmt = $dbh->prepare('SELECT * FROM schools WHERE id = :id'); // 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->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
@@ -98,7 +114,7 @@ function afficherEcoleCard($ecole, $admin = false) {
if ($admin) { if ($admin) {
echo '<div class="admin-actions">'; 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-edit" href="./admin.php?edit_school=' . $id . '#admin-ecoles">Modifier</a>';
echo '<a class="btn-submit btn-delete" href="../scripts/removeSchool.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette école ?\');">Supprimer</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>';
} }
+1 -1
View File
@@ -99,7 +99,7 @@ function afficherSkillCard($skill, $admin = false) {
if ($admin) { if ($admin) {
echo '<div class="admin-actions">'; 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-edit" href="./admin.php?edit_skill=' . $id . '#admin-competences">Modifier</a>';
echo '<a class="btn-submit btn-delete" href="../scripts/removeSkill.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette catégorie de compétences ?\');">Supprimer</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>';
} }
+27
View File
@@ -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;
}
+1 -1
View File
@@ -19,7 +19,7 @@ function afficherUsersAdmin()
echo '<p><strong>Créé le :</strong> ' . htmlspecialchars($user["created_at"]) . '</p>'; echo '<p><strong>Créé le :</strong> ' . htmlspecialchars($user["created_at"]) . '</p>';
echo '<div class="admin-actions">'; 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>';
echo '</div>'; echo '</div>';
+21 -7
View File
@@ -13,10 +13,17 @@ function listeXP() {
// Essayer // Essayer
try { try {
// Requête SQL qui récupère titre, type, tags, description, durée, image_url // On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne // stocké à part dans experience_pdfs (voir migration Juillet 2026 (6)), on ne
// même si la migration SQL n'a pas encore été exécutée) // récupère ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu
$stmt = $dbh->prepare('SELECT * FROM experiences ORDER BY id DESC'); // 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 // Execution de la requête SQL
$stmt->execute(); $stmt->execute();
// Retourne toutes les données collectés dans un ARRAY // Retourne toutes les données collectés dans un ARRAY
@@ -32,8 +39,15 @@ function recupererXPParId($id) {
global $dbh; global $dbh;
try { try {
// Préparation de la requête préparée // Même principe que listeXP() : le PDF est joint pour son nom de fichier
$stmt = $dbh->prepare('SELECT * FROM experiences WHERE id = :id'); // 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->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
@@ -195,7 +209,7 @@ function afficherXPAdmin() {
// Boutons qui vont permettrent la modification / Suppresion // Boutons qui vont permettrent la modification / Suppresion
echo '<div class="admin-actions">'; echo '<div class="admin-actions">';
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '#admin-experiences">Modifier</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 . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette expérience ?\');">Supprimer</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>';
echo '</div>'; echo '</div>';
+48
View File
@@ -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.");
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
// Le header n'y étant pas je rajoute ici un démarrage de session // 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 // Redirection de l'utilisateur vers une page Home lorsqu'il arrive sur index.php
header('location: ./pages/home.php'); header('location: ./pages/home.php');
+9 -1
View File
@@ -87,6 +87,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<!-- enctype multipart/form-data : nécessaire pour l'upload du PDF --> <!-- enctype multipart/form-data : nécessaire pour l'upload du PDF -->
<!-- en envoie les données à addProject.php --> <!-- en envoie les données à addProject.php -->
<form method="POST" enctype="multipart/form-data" action="<?php echo $projetAModifier ? '../scripts/updateProject.php' : '../scripts/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): ?> <?php if ($projetAModifier): ?>
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
@@ -151,6 +152,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<div class="project-form-card"> <div class="project-form-card">
<!-- en envoie les données à addXP.php --> <!-- en envoie les données à addXP.php -->
<form method="POST" enctype="multipart/form-data" action="<?php echo $xpAModifier ? '../scripts/updateXP.php' : '../scripts/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): ?> <?php if ($xpAModifier): ?>
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
@@ -221,6 +223,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<div class="project-form-card"> <div class="project-form-card">
<!-- on envoie les données à addSchool.php --> <!-- on envoie les données à addSchool.php -->
<form method="POST" enctype="multipart/form-data" action="<?php echo $ecoleAModifier ? '../scripts/updateSchool.php' : '../scripts/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): ?> <?php if ($ecoleAModifier): ?>
<input type="hidden" name="id" value="<?php echo (int) $ecoleAModifier['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int) $ecoleAModifier['id']; ?>">
@@ -288,6 +291,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<div class="project-form-card"> <div class="project-form-card">
<!-- on envoie les données à addSkill.php --> <!-- on envoie les données à addSkill.php -->
<form method="POST" action="<?php echo $skillAModifier ? '../scripts/updateSkill.php' : '../scripts/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): ?> <?php if ($skillAModifier): ?>
<input type="hidden" name="id" value="<?php echo (int) $skillAModifier['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int) $skillAModifier['id']; ?>">
@@ -332,6 +336,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<div class="project-form-card"> <div class="project-form-card">
<!-- on envoie les données à addLogo.php --> <!-- on envoie les données à addLogo.php -->
<form method="POST" action="<?php echo $logoAModifier ? '../scripts/updateLogo.php' : '../scripts/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): ?> <?php if ($logoAModifier): ?>
<input type="hidden" name="id" value="<?php echo (int) $logoAModifier['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int) $logoAModifier['id']; ?>">
@@ -376,6 +381,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<div class="project-form-card"> <div class="project-form-card">
<!-- on envoie les données à updateSettings.php --> <!-- on envoie les données à updateSettings.php -->
<form method="POST" action="../scripts/updateSettings.php"> <form method="POST" action="../scripts/updateSettings.php">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
<label>Titre (affiché en gras)</label> <label>Titre (affiché en gras)</label>
<input type="text" name="notice_title" value="<?php echo htmlspecialchars($noticeTitle); ?>"> <input type="text" name="notice_title" value="<?php echo htmlspecialchars($noticeTitle); ?>">
@@ -406,6 +412,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<div class="project-form-card"> <div class="project-form-card">
<!-- en envoie les données addUser.php --> <!-- en envoie les données addUser.php -->
<form method="POST" action="../scripts/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> <label>Nom du compte</label>
<input type="text" name="user" required> <input type="text" name="user" required>
@@ -440,7 +447,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
Télécharge une sauvegarde JSON de toutes les données du site Télécharge une sauvegarde JSON de toutes les données du site
(projets, expériences, écoles, compétences, notification, utilisateurs). (projets, expériences, écoles, compétences, notification, utilisateurs).
</p> </p>
<a class="btn-submit admin-export-btn" href="../scripts/exportData.php"> <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) <i class="fa-solid fa-download"></i> Exporter les données (JSON)
</a> </a>
@@ -454,6 +461,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
</p> </p>
<form method="POST" action="../scripts/importData.php" enctype="multipart/form-data" <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 ?');"> 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="file" name="jsonfile" accept=".json,application/json" required>
<input type="submit" value="Importer les données" class="btn-submit btn-delete"> <input type="submit" value="Importer les données" class="btn-submit btn-delete">
</form> </form>
+14 -7
View File
@@ -8,10 +8,12 @@
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// Types autorisés : liste blanche pour empêcher toute injection de nom de table // 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 = [ $types = [
'projet' => ['table' => 'projects', 'retour' => './projects.php', 'label' => 'Projet'], 'projet' => ['table' => 'projects', 'pdf' => 'project_pdfs', 'pdf_colonne' => 'project_id', 'retour' => './projects.php', 'label' => 'Projet'],
'experience' => ['table' => 'experiences', 'retour' => './xp.php', 'label' => 'Expérience'], 'experience' => ['table' => 'experiences', 'pdf' => 'experience_pdfs', 'pdf_colonne' => 'experience_id', 'retour' => './xp.php', 'label' => 'Expérience'],
'ecole' => ['table' => 'schools', 'retour' => './xp.php', 'label' => 'École'], 'ecole' => ['table' => 'schools', 'pdf' => 'school_pdfs', 'pdf_colonne' => 'school_id', 'retour' => './xp.php', 'label' => 'École'],
]; ];
$type = $_GET['type'] ?? ''; $type = $_GET['type'] ?? '';
@@ -28,15 +30,20 @@ $infos = $types[$type];
// Récupération de l'élément demandé // Récupération de l'élément demandé
// (pdf_data n'est volontairement pas sélectionné : le PDF est servi à part par viewPdf.php // (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) // dans une <iframe>, inutile de charger son contenu binaire en mémoire ici)
$colonnesCommunes = 'id, title, status, tags, description, image_url, markdown, link_url, pdf_filename'; $colonnesCommunes = 'e.id, e.title, e.status, e.tags, e.description, e.image_url, e.markdown, e.link_url, p.pdf_filename';
$colonnesParType = [ $colonnesParType = [
'projet' => $colonnesCommunes, 'projet' => $colonnesCommunes,
'experience' => $colonnesCommunes . ', xptype, duration', 'experience' => $colonnesCommunes . ', e.xptype, e.duration',
'ecole' => $colonnesCommunes . ', duration', 'ecole' => $colonnesCommunes . ', e.duration',
]; ];
try { try {
$stmt = $dbh->prepare('SELECT ' . $colonnesParType[$type] . ' FROM `' . $infos['table'] . '` WHERE id = :id'); $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->bindValue(':id', (int) $id, PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
$element = $stmt->fetch(PDO::FETCH_ASSOC); $element = $stmt->fetch(PDO::FETCH_ASSOC);
+27
View File
@@ -19,6 +19,12 @@ require_once('../functions/listLogos.php');
// Compétences, pas de saisie séparée (voir listeIconesCompetences) // Compétences, pas de saisie séparée (voir listeIconesCompetences)
require_once('../functions/listSkills.php'); 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) // Affichage de la notification (si elle est activée dans le panel Admin)
afficherNotice(); afficherNotice();
?> ?>
@@ -42,6 +48,27 @@ afficherNotice();
</a> </a>
</section> </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())): ?> <?php if (!empty(listeIconesCompetences())): ?>
<!-- Bandeau défilant des outils, dérivé de la page Compétences --> <!-- Bandeau défilant des outils, dérivé de la page Compétences -->
<section class="logo-banner-section"> <section class="logo-banner-section">
+4 -1
View File
@@ -1,5 +1,5 @@
<?php <?php
session_start(); require_once(__DIR__ . '/../functions/security.php');
require_once '../functions/databaseConnection.php'; require_once '../functions/databaseConnection.php';
require_once('../functions/sendLogs.php'); require_once('../functions/sendLogs.php');
@@ -9,6 +9,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // La requête doit être un POST sinon on l'a refuse
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+18 -4
View File
@@ -1,5 +1,5 @@
<?php <?php
session_start(); require_once(__DIR__ . '/../functions/security.php');
require_once '../functions/databaseConnection.php'; require_once '../functions/databaseConnection.php';
require_once('../functions/sendLogs.php'); require_once('../functions/sendLogs.php');
require_once('../functions/pdfUpload.php'); require_once('../functions/pdfUpload.php');
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
// tout de suite pour éviter un échec silencieux // tout de suite pour éviter un échec silencieux
if (postDepasseLimite()) { if (postDepasseLimite()) {
@@ -42,7 +45,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($title) && !empty($description) && !empty($image_url)) { if (!empty($title) && !empty($description) && !empty($image_url)) {
try { try {
// On prépare la requête SQL pour insérer le nouveau projet // On prépare la requête SQL pour insérer le nouveau projet
$stmt = $dbh->prepare('INSERT INTO projects (title, status, tags, description, image_url, markdown, link_url, pdf_data, pdf_filename) VALUES (:title, :status, :tags, :description, :image_url, :markdown, :link_url, :pdf_data, :pdf_filename)'); // 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 // On execute la requete préparée
$stmt->bindValue(':title', $title, PDO::PARAM_STR); $stmt->bindValue(':title', $title, PDO::PARAM_STR);
@@ -52,10 +58,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR); $stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR); $stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR); $stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
$stmt->execute(); $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 // Log Discord
sendLog("Ajout du projet : " . $title); sendLog("Ajout du projet : " . $title);
+18 -4
View File
@@ -1,5 +1,5 @@
<?php <?php
session_start(); require_once(__DIR__ . '/../functions/security.php');
require_once '../functions/databaseConnection.php'; require_once '../functions/databaseConnection.php';
require_once('../functions/sendLogs.php'); require_once('../functions/sendLogs.php');
require_once('../functions/pdfUpload.php'); require_once('../functions/pdfUpload.php');
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
// tout de suite pour éviter un échec silencieux // tout de suite pour éviter un échec silencieux
if (postDepasseLimite()) { if (postDepasseLimite()) {
@@ -42,7 +45,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($title) && !empty($description) && !empty($duration) && !empty($image_url)) { if (!empty($title) && !empty($description) && !empty($duration) && !empty($image_url)) {
try { try {
// On prépare la requête SQL pour insérer la nouvelle école // On prépare la requête SQL pour insérer la nouvelle école
$stmt = $dbh->prepare('INSERT INTO schools (title, status, tags, description, duration, image_url, markdown, link_url, pdf_data, pdf_filename) VALUES (:title, :status, :tags, :description, :duration, :image_url, :markdown, :link_url, :pdf_data, :pdf_filename)'); // 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 // On execute la requete préparée
$stmt->bindValue(':title', $title, PDO::PARAM_STR); $stmt->bindValue(':title', $title, PDO::PARAM_STR);
@@ -53,10 +59,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR); $stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR); $stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR); $stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
$stmt->execute(); $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 // Log Discord
sendLog("Ajout de l'école : " . $title); sendLog("Ajout de l'école : " . $title);
+4 -1
View File
@@ -1,5 +1,5 @@
<?php <?php
session_start(); require_once(__DIR__ . '/../functions/security.php');
require_once '../functions/databaseConnection.php'; require_once '../functions/databaseConnection.php';
require_once('../functions/sendLogs.php'); require_once('../functions/sendLogs.php');
@@ -9,6 +9,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // La requête doit être un POST sinon on l'a refuse
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// Démarrage de la session pour vérifier les droits de l'utilisateur // 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 // Ajout de connexion à la BDD
require_once("../functions/databaseConnection.php"); require_once("../functions/databaseConnection.php");
// Ajout des logs Discord // Ajout des logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
exit(); exit();
} }
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
requireCsrfToken();
// Si le formulaire a été soumis // Si le formulaire a été soumis
if ($_SERVER["REQUEST_METHOD"] == "POST") { if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Récupération des données du formulaire // Récupération des données du formulaire
+18 -4
View File
@@ -1,5 +1,5 @@
<?php <?php
session_start(); require_once(__DIR__ . '/../functions/security.php');
require_once '../functions/databaseConnection.php'; require_once '../functions/databaseConnection.php';
require_once('../functions/sendLogs.php'); require_once('../functions/sendLogs.php');
require_once('../functions/pdfUpload.php'); require_once('../functions/pdfUpload.php');
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
// tout de suite pour éviter un échec silencieux // tout de suite pour éviter un échec silencieux
if (postDepasseLimite()) { if (postDepasseLimite()) {
@@ -43,7 +46,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($title) && !empty($description) && !empty($image_url)) { if (!empty($title) && !empty($description) && !empty($image_url)) {
try { try {
// On prépare la requête SQL pour insérer la nouvelle experience // On prépare la requête SQL pour insérer la nouvelle experience
$stmt = $dbh->prepare('INSERT INTO experiences (title, status, xptype, tags, description, duration, image_url, markdown, link_url, pdf_data, pdf_filename) VALUES (:title, :status, :xptype, :tags, :description, :duration, :image_url, :markdown, :link_url, :pdf_data, :pdf_filename)'); // 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 // On execute la requete préparée
$stmt->bindValue(':title', $title, PDO::PARAM_STR); $stmt->bindValue(':title', $title, PDO::PARAM_STR);
@@ -55,10 +61,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR); $stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR); $stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR); $stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
$stmt->execute(); $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 // Log Discord
sendLog("Ajout de l'expérience : " . $title); sendLog("Ajout de l'expérience : " . $title);
+6 -1
View File
@@ -3,7 +3,7 @@
// Verification de l'utilisateur voir si ces ids sont bon ou pas // 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 // 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 // Ajout de connexion a la bdd
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
@@ -38,6 +38,11 @@ if (!$user || !password_verify($password, $user['password'])) {
exit; 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 // Si la connexion est ok, on entregistre l'utilisateur dans $_SESSION
$_SESSION['user_id'] = $user['id']; $_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username']; $_SESSION['username'] = $user['username'];
+3 -2
View File
@@ -2,8 +2,8 @@
// Lorsque l'utilisateur est renvoyé à cette page/fonction, il est déconnecté // Lorsque l'utilisateur est renvoyé à cette page/fonction, il est déconnecté
// Démarrage de session // Démarrage de session (cookie durci)
session_start(); require_once(__DIR__ . '/../functions/security.php');
// Ajout des logs Discord // Ajout des logs Discord
require_once('../functions/sendLogs.php'); require_once('../functions/sendLogs.php');
@@ -21,6 +21,7 @@ session_destroy();
// Si le captcha avait déjà été validé, on recrée une session propre // Si le captcha avait déjà été validé, on recrée une session propre
// pour conserver uniquement cette information // pour conserver uniquement cette information
if ($captchaAlreadyValid) { if ($captchaAlreadyValid) {
configureSecureSessionCookies();
session_start(); session_start();
$_SESSION['captcha_valid'] = true; $_SESSION['captcha_valid'] = true;
} }
+20 -10
View File
@@ -3,7 +3,7 @@
// Le fichier téléchargé peut ensuite être ré-importé via importData.php // Le fichier téléchargé peut ensuite être ré-importé via importData.php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -15,22 +15,32 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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) // 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 = [ $tables = [
'settings' => ['name', 'value'], 'settings' => ['name', 'value'],
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'], 'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'], 'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'], 'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'], 'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'], '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 // 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) // (+33% de taille mais nécessaire pour que le PDF survive à un export/import complet)
$colonnesBinaires = [ $colonnesBinaires = [
'projects' => ['pdf_data'], 'project_pdfs' => ['pdf_data'],
'experiences' => ['pdf_data'], 'experience_pdfs' => ['pdf_data'],
'schools' => ['pdf_data'], 'school_pdfs' => ['pdf_data'],
]; ];
// Métadonnées de l'export (pratique pour s'y retrouver dans les sauvegardes) // Métadonnées de l'export (pratique pour s'y retrouver dans les sauvegardes)
+20 -11
View File
@@ -3,7 +3,7 @@
// ATTENTION : les données actuelles des tables présentes dans le fichier sont ÉCRASÉES // ATTENTION : les données actuelles des tables présentes dans le fichier sont ÉCRASÉES
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -15,6 +15,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // 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) { if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['jsonfile']) || $_FILES['jsonfile']['error'] !== UPLOAD_ERR_OK) {
header('Location: ../pages/admin.php'); header('Location: ../pages/admin.php');
@@ -29,21 +32,27 @@ if (!is_array($donnees)) {
die("Erreur : le fichier n'est pas un JSON valide."); 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 // 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 = [ $tables = [
'settings' => ['name', 'value'], 'settings' => ['name', 'value'],
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'], 'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'], 'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'], 'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'], 'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'], '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 // Colonnes binaires : encodées en base64 par exportData.php, à redécoder ici
$colonnesBinaires = [ $colonnesBinaires = [
'projects' => ['pdf_data'], 'project_pdfs' => ['pdf_data'],
'experiences' => ['pdf_data'], 'experience_pdfs' => ['pdf_data'],
'schools' => ['pdf_data'], 'school_pdfs' => ['pdf_data'],
]; ];
try { try {
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 //L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header('Location: ../pages/admin.php'); header('Location: ../pages/admin.php');
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 //L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header('Location: ../pages/admin.php'); header('Location: ../pages/admin.php');
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 //L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header('Location: ../pages/admin.php'); header('Location: ../pages/admin.php');
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 //L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header('Location: ../pages/admin.php'); header('Location: ../pages/admin.php');
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// Démarrage de la session pour vérifier les droits de l'utilisateur // 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 // Ajout de connexion à la BDD
require_once("../functions/databaseConnection.php"); require_once("../functions/databaseConnection.php");
// Ajout des logs Discord // Ajout des logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
exit(); 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 // Récupération de l'ID passé en paramètre GET
$id = $_GET["id"]; $id = $_GET["id"];
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 //L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header('Location: ../pages/admin.php'); header('Location: ../pages/admin.php');
+1 -1
View File
@@ -3,7 +3,7 @@
require_once('../../idDiscord.php'); require_once('../../idDiscord.php');
// Ajout des logs Discord // Ajout des logs Discord
session_start(); require_once(__DIR__ . '/../functions/security.php');
require_once('../functions/sendLogs.php'); require_once('../functions/sendLogs.php');
// Envoi de formulaire de contact via WebHook discord // Envoi de formulaire de contact via WebHook discord
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 if ( // Si les champs sont vides on renvoie vers admin.php
!isset($_POST['id']) || !is_numeric($_POST['id']) || !isset($_POST['id']) || !is_numeric($_POST['id']) ||
empty($_POST['name']) || empty($_POST['name']) ||
+29 -13
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // 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) // tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
if (postDepasseLimite()) { if (postDepasseLimite()) {
@@ -61,15 +64,13 @@ if (is_array($pdfResultat)) {
} }
try { // On essaie d'executer une requête qui est préparée try { // On essaie d'executer une requête qui est préparée
$sql = ' // 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 UPDATE projects
SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url'; SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url
if ($updatePdf) { WHERE id = :id
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename'; ');
}
$sql .= ' WHERE id = :id';
$stmt = $dbh->prepare($sql);
$stmt->bindValue(':title', $title, PDO::PARAM_STR); $stmt->bindValue(':title', $title, PDO::PARAM_STR);
$stmt->bindValue(':status', $status, PDO::PARAM_STR); $stmt->bindValue(':status', $status, PDO::PARAM_STR);
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR); $stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
@@ -77,12 +78,27 @@ try { // On essaie d'executer une requête qui est préparée
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR); $stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR); $stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR); $stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
if ($updatePdf) {
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
}
$stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute(); // Sinon on renvoie l'erreur $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) { } catch (PDOException $e) {
die("Erreur lors de la modification du projet :" . $e->getMessage()); die("Erreur lors de la modification du projet :" . $e->getMessage());
} }
+29 -13
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // 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) // tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
if (postDepasseLimite()) { if (postDepasseLimite()) {
@@ -62,15 +65,13 @@ if (is_array($pdfResultat)) {
} }
try { // On essaie d'executer une requête qui est préparée try { // On essaie d'executer une requête qui est préparée
$sql = ' // 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 UPDATE schools
SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url'; SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
if ($updatePdf) { WHERE id = :id
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename'; ');
}
$sql .= ' WHERE id = :id';
$stmt = $dbh->prepare($sql);
$stmt->bindValue(':title', $title, PDO::PARAM_STR); $stmt->bindValue(':title', $title, PDO::PARAM_STR);
$stmt->bindValue(':status', $status, PDO::PARAM_STR); $stmt->bindValue(':status', $status, PDO::PARAM_STR);
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR); $stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
@@ -79,12 +80,27 @@ try { // On essaie d'executer une requête qui est préparée
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR); $stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR); $stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR); $stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
if ($updatePdf) {
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
}
$stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute(); // Sinon on renvoie l'erreur $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) { } catch (PDOException $e) {
die("Erreur lors de la modification de l'école :" . $e->getMessage()); die("Erreur lors de la modification de l'école :" . $e->getMessage());
} }
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // La requête doit être un POST sinon on l'a refuse
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/admin.php'); header('Location: ../pages/admin.php');
+4 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 if ( // Si les champs sont vides on renvoie vers admin.php
!isset($_POST['id']) || !is_numeric($_POST['id']) || !isset($_POST['id']) || !is_numeric($_POST['id']) ||
empty($_POST['title']) || empty($_POST['title']) ||
+29 -13
View File
@@ -1,6 +1,6 @@
<?php <?php
// On démarre la session // On démarre la session
session_start(); require_once(__DIR__ . '/../functions/security.php');
// On ajoute la DB // On ajoute la DB
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// On ajoute les logs Discord // On ajoute les logs Discord
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
exit; 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 // 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) // tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
if (postDepasseLimite()) { if (postDepasseLimite()) {
@@ -65,15 +68,13 @@ if (is_array($pdfResultat)) {
} }
try { // On essaie d'executer une requête qui est préparée try { // On essaie d'executer une requête qui est préparée
$sql = ' // 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 UPDATE experiences
SET title = :title, status = :status, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url'; SET title = :title, status = :status, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
if ($updatePdf) { WHERE id = :id
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename'; ');
}
$sql .= ' WHERE id = :id';
$stmt = $dbh->prepare($sql);
$stmt->bindValue(':title', $title, PDO::PARAM_STR); $stmt->bindValue(':title', $title, PDO::PARAM_STR);
$stmt->bindValue(':status', $status, PDO::PARAM_STR); $stmt->bindValue(':status', $status, PDO::PARAM_STR);
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR); $stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
@@ -83,12 +84,27 @@ try { // On essaie d'executer une requête qui est préparée
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR); $stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR); $stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR); $stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
if ($updatePdf) {
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
}
$stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute(); // Sinon on renvoie l'erreur $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) { } catch (PDOException $e) {
die("Erreur lors de la modification de l experience :" . $e->getMessage()); die("Erreur lors de la modification de l experience :" . $e->getMessage());
} }
+6 -4
View File
@@ -6,10 +6,12 @@
require_once('../functions/databaseConnection.php'); require_once('../functions/databaseConnection.php');
// Liste blanche des types autorisés : empêche toute injection de nom de table // 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 = [ $tables = [
'projet' => 'projects', 'projet' => ['pdf' => 'project_pdfs', 'colonne' => 'project_id'],
'experience' => 'experiences', 'experience' => ['pdf' => 'experience_pdfs', 'colonne' => 'experience_id'],
'ecole' => 'schools', 'ecole' => ['pdf' => 'school_pdfs', 'colonne' => 'school_id'],
]; ];
$type = $_GET['type'] ?? ''; $type = $_GET['type'] ?? '';
@@ -21,7 +23,7 @@ if (!isset($tables[$type]) || !is_numeric($id)) {
} }
try { try {
$stmt = $dbh->prepare('SELECT pdf_data, pdf_filename FROM `' . $tables[$type] . '` WHERE id = :id'); $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->bindValue(':id', (int) $id, PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
+38
View File
@@ -517,6 +517,44 @@ img {
flex-wrap: wrap; flex-wrap: wrap;
} }
/* ---- Bandeau de chiffres clés (page d'accueil) ---- */
.stats-strip {
max-width: 900px;
margin: 0 auto;
padding: 0 20px 70px;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 20px;
text-align: center;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 18px 34px;
min-width: 140px;
border-radius: var(--radius);
background: var(--surface);
border: 1px solid var(--border);
}
.stat-number {
font-size: clamp(28px, 4vw, 38px);
font-weight: 800;
letter-spacing: -0.02em;
color: var(--accent);
}
.stat-label {
font-size: 13.5px;
color: var(--text-muted);
font-weight: 600;
}
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;