Compare commits

...
9 Commits
Author SHA1 Message Date
Esteban e5bd31fc65 Patch gitignore 2026-08-08 19:36:29 +02:00
Esteban a4ccd7e2a9 Remove html file 2026-07-11 04:59:37 +02:00
Esteban e26dbd388f Changement d'adresse dynamique 2026-07-09 14:40:26 +02:00
Esteban a1c99f1de8 Suppression easteregg + fix captcha 2026-07-09 11:54:37 +02:00
Esteban c04a9b5ab8 Ajout d'un easteregg 2026-07-03 10:56:54 +02:00
Esteban c28fae7061 Patch php 2026-07-03 02:05:18 +02:00
Esteban 91060537c8 Fix lag 2026-07-03 02:02:13 +02:00
Esteban 5b76997895 Patch 2026-07-03 00:23:54 +02:00
Esteban 4c35c727d5 Sauvegarde 2026-07-03 00:23:37 +02:00
45 changed files with 979 additions and 277 deletions

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# Config Claude Code
.claude/
# macOS
.DS_Store
**/.DS_Store
# Éditeurs
.vscode/
.idea/
*.iml
# Logs
*.log
logs/
+13 -10
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');
@@ -180,16 +180,19 @@ $spamSubjects = [
['text' => 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT', 'reason' => 'Promesse d’argent contre des données personnelles'], ['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 = [ $spamSenders = [
'service-urgent@compte-bloque-now.biz', 'Sécurité du compte',
'cadeau-gratuit@super-offre-win.click', 'Service client',
'banque-alerte@verification-securite.top', 'Banque familiale',
'colis-stop@paiement-obligatoire.help', 'Livraison Colis',
'admin-compte@action-immediate.world', 'Support technique',
'support-prime@argent-rapide-fast.support', 'Service facturation',
'verification@motdepasse-danger.win', 'Assurance Habitation',
'alerte@compte-fermeture.click' 'Mutuelle Santé'
]; ];
// Liste des aperçus normaux // Liste des aperçus normaux
+63 -89
View File
@@ -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) -- Mot de passe à changer après exécution du script (voir Bitwarden)
CREATE USER 'srv_portfolio'@'%' IDENTIFIED BY 'mdp'; CREATE USER IF NOT EXISTS 'srv_portfolio'@'%' IDENTIFIED BY 'mdp';
-- On met les droits au compte sur la base portfolio à l'utilisateur
GRANT ALL ON portfolio.* TO 'srv_portfolio'@'%'; GRANT ALL ON portfolio.* TO 'srv_portfolio'@'%';
-- On actualise les permissions
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
-- Création de la table user : (Généré par Beekeeper Studio : équivalent à dbGate)
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT, id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL, username VARCHAR(50) NOT NULL,
@@ -18,63 +13,75 @@ CREATE TABLE IF NOT EXISTS users (
role ENUM('admin', 'user') NOT NULL DEFAULT 'user', role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
created_at DATETIME NULL DEFAULT NULL, created_at DATETIME NULL DEFAULT NULL,
last_login DATETIME NULL DEFAULT NULL, last_login DATETIME NULL DEFAULT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE INDEX idx_username (username) 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 ( CREATE TABLE IF NOT EXISTS projects (
id INT UNSIGNED NOT NULL AUTO_INCREMENT, id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(100) NOT NULL, title VARCHAR(100) NOT NULL,
status VARCHAR(100) NULL,
tags VARCHAR(255) NULL, tags VARCHAR(255) NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
image_url VARCHAR(255) NOT NULL, image_url VARCHAR(255) NOT NULL,
markdown LONGTEXT NULL,
link_url VARCHAR(255) NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id) PRIMARY KEY (id)
); );
-- Création de la table projects : (Généré par Beekeeper Studio)
CREATE TABLE IF NOT EXISTS experiences ( CREATE TABLE IF NOT EXISTS experiences (
id INT UNSIGNED NOT NULL AUTO_INCREMENT, id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(100) NOT NULL, title VARCHAR(100) NOT NULL,
status VARCHAR(100) NULL,
xptype VARCHAR(100) NOT NULL, xptype VARCHAR(100) NOT NULL,
tags VARCHAR(255) NULL, tags VARCHAR(255) NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
duration VARCHAR(100) NOT NULL, duration VARCHAR(100) NOT NULL,
image_url VARCHAR(255) NOT NULL, image_url VARCHAR(255) NOT NULL,
markdown LONGTEXT NULL,
link_url VARCHAR(255) NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id) PRIMARY KEY (id)
); );
-- Compte de test (PLUS A JOUR METTRE UN HASH) CREATE TABLE IF NOT EXISTS schools (
-- INSERT INTO users (username, password, role) id INT UNSIGNED NOT NULL AUTO_INCREMENT,
-- VALUES ('admin', 'admin', 'admin'); title VARCHAR(100) NOT NULL,
insert into `users` (`created_at`, `id`, `last_login`, `password`, `role`, `username`) status VARCHAR(100) NULL,
values ('2026-06-08 11:10:21', 11, '2026-06-08 13:10:41', '$2y$12$x01fDqrHQEYIyj7MDHa72eRyl2j/n5cNq9sv3.FIiQCaRsB50Q4iK', 'admin', 'admin'); 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 -- Réglages du site (clé / valeur), utilisés pour la notification de la page d'accueil et le captcha
ALTER TABLE projects
ADD COLUMN status VARCHAR(100) NULL AFTER title;
ALTER TABLE experiences
ADD COLUMN status VARCHAR(100) NULL AFTER title;
-- Table de réglages du site (clé / valeur) : utilisée pour la notification de la page d'accueil
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
name VARCHAR(50) NOT NULL, name VARCHAR(50) NOT NULL,
value TEXT NULL, value TEXT NULL,
PRIMARY KEY (name) PRIMARY KEY (name)
); );
-- Valeurs par défaut de la notification (modifiables ensuite depuis le panel Admin)
INSERT INTO settings (name, value) VALUES INSERT INTO settings (name, value) VALUES
('site_notice_title', 'Des bugs peuvent encore se produire.'), ('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_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') ('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); ON DUPLICATE KEY UPDATE value = VALUES(value);
-- Table des compétences : une ligne = une catégorie (carte sur la page Compétences) -- items : un item par ligne, au format "classes-icone|Texte" (icône optionnelle)
-- items : un item par ligne, au format "classes-icone-fontawesome|Texte" (l'icône est optionnelle : "Texte" seul fonctionne aussi)
CREATE TABLE IF NOT EXISTS skills ( CREATE TABLE IF NOT EXISTS skills (
id INT UNSIGNED NOT NULL AUTO_INCREMENT, id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(100) NOT NULL, title VARCHAR(100) NOT NULL,
@@ -85,7 +92,6 @@ CREATE TABLE IF NOT EXISTS skills (
PRIMARY KEY (id) PRIMARY KEY (id)
); );
-- Compétences actuelles (reprises du CV)
INSERT INTO skills (title, icon, items, position) VALUES 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), ('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), ('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),
@@ -94,75 +100,43 @@ INSERT INTO skills (title, icon, items, position) VALUES
('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), ('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); ('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);
-- Table des écoles / formations : affichée sous les expériences professionnelles sur la page Expériences -- category : 'tool' (bandeau "Outils que je sais manipuler") ou 'partner' (bandeau "Ils m'ont fait confiance")
CREATE TABLE IF NOT EXISTS schools ( -- icon : classes CSS Devicon, prioritaire sur logo_url si les deux sont renseignés (au moins un des deux requis)
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,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
-- markdown : contenu de la page détaillée (LONGTEXT = ~4 Go, taille pratiquement illimitée)
-- link_url : lien externe utilisé à la place du markdown si celui-ci est vide
-- Les deux sont facultatifs : si les deux sont vides, la carte n'est pas cliquable
ALTER TABLE projects
ADD COLUMN markdown LONGTEXT NULL,
ADD COLUMN link_url VARCHAR(255) NULL;
ALTER TABLE experiences
ADD COLUMN markdown LONGTEXT NULL,
ADD COLUMN link_url VARCHAR(255) NULL;
ALTER TABLE schools
ADD COLUMN markdown LONGTEXT NULL,
ADD COLUMN link_url VARCHAR(255) NULL;
-- ============================================================
-- Juillet 2026 (4) : bandeaux de logos sur la page d'accueil
-- ("Outils que je sais manipuler" et "Ils m'ont fait confiance"),
-- gérés depuis le panel Admin
-- ============================================================
-- Une seule table pour les deux bandeaux, distingués par la colonne category :
-- - 'tool' => bandeau "Outils que je sais manipuler"
-- - 'partner' => bandeau "Ils m'ont fait confiance"
-- link_url est facultatif : si renseigné, le logo devient cliquable (nouvel onglet)
CREATE TABLE IF NOT EXISTS logo_banners ( CREATE TABLE IF NOT EXISTS logo_banners (
id INT UNSIGNED NOT NULL AUTO_INCREMENT, id INT UNSIGNED NOT NULL AUTO_INCREMENT,
category ENUM('tool', 'partner') NOT NULL, category ENUM('tool', 'partner') NOT NULL,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
logo_url VARCHAR(255) NOT NULL, icon VARCHAR(100) NULL,
logo_url VARCHAR(255) NULL,
link_url VARCHAR(255) NULL, link_url VARCHAR(255) NULL,
position INT NOT NULL DEFAULT 0, position INT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id) PRIMARY KEY (id)
); );
-- Table créée vide volontairement : les logos sont ajoutés depuis le panel Admin -- PDF de projects/experiences/schools : tables associées plutôt qu'une colonne sur la table
-- (section Logos) pour éviter d'afficher des images devinées/potentiellement cassées -- 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 (
-- Juillet 2026 (3) : PDF en drag & drop pour les projets, experience_id INT UNSIGNED NOT NULL,
-- expériences et écoles, lu depuis un mini lecteur intégré 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
);
-- pdf_data : contenu binaire du PDF (LONGBLOB = ~4 Go, largement suffisant) CREATE TABLE IF NOT EXISTS school_pdfs (
-- pdf_filename : nom original du fichier (affiché dans le panel admin et le lecteur) school_id INT UNSIGNED NOT NULL,
-- Priorité d'affichage sur la page de détails : markdown > pdf > lien externe pdf_filename VARCHAR(255) NOT NULL,
ALTER TABLE projects pdf_data LONGBLOB NOT NULL,
ADD COLUMN pdf_data LONGBLOB NULL, PRIMARY KEY (school_id),
ADD COLUMN pdf_filename VARCHAR(255) NULL; CONSTRAINT fk_school_pdfs_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
);
ALTER TABLE experiences
ADD COLUMN pdf_data LONGBLOB NULL,
ADD COLUMN pdf_filename VARCHAR(255) NULL;
ALTER TABLE schools
ADD COLUMN pdf_data LONGBLOB NULL,
ADD COLUMN pdf_filename VARCHAR(255) NULL;
+14
View File
@@ -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
+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) {
+29 -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">
@@ -52,6 +77,9 @@ $currentPage = basename($_SERVER['PHP_SELF']);
<link rel="stylesheet" href="../styles/primary_css.css"> <link rel="stylesheet" href="../styles/primary_css.css">
<!-- Ajout de Font Awesome pour les icones de home.php--> <!-- 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"> <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> </head>
<body> <body>
+24 -8
View File
@@ -12,11 +12,13 @@ function listeLogos($category = null) {
global $dbh; global $dbh;
try { 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) { if ($category !== null) {
$stmt = $dbh->prepare('SELECT id, category, name, logo_url, link_url, position FROM logo_banners WHERE category = :category ORDER BY position ASC, id ASC'); $stmt = $dbh->prepare('SELECT * FROM logo_banners WHERE category = :category ORDER BY position ASC, id ASC');
$stmt->bindValue(':category', $category, PDO::PARAM_STR); $stmt->bindValue(':category', $category, PDO::PARAM_STR);
} else { } else {
$stmt = $dbh->prepare('SELECT id, category, name, logo_url, link_url, position FROM logo_banners ORDER BY category ASC, position ASC, id ASC'); $stmt = $dbh->prepare('SELECT * FROM logo_banners ORDER BY category ASC, position ASC, id ASC');
} }
$stmt->execute(); $stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -30,7 +32,7 @@ function recupererLogoParId($id) {
global $dbh; global $dbh;
try { try {
$stmt = $dbh->prepare('SELECT id, category, name, logo_url, link_url, position FROM logo_banners WHERE id = :id'); $stmt = $dbh->prepare('SELECT * FROM logo_banners WHERE 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);
@@ -39,6 +41,20 @@ function recupererLogoParId($id) {
} }
} }
// 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 // 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) // N'affiche rien si la catégorie est vide (la section entière est masquée côté page d'accueil)
function afficherLogos($category) { function afficherLogos($category) {
@@ -58,7 +74,6 @@ function afficherLogos($category) {
for ($repetition = 0; $repetition < 2; $repetition++) { for ($repetition = 0; $repetition < 2; $repetition++) {
foreach ($logos as $logo) { foreach ($logos as $logo) {
$nom = htmlspecialchars($logo['name']); $nom = htmlspecialchars($logo['name']);
$image = htmlspecialchars($logo['logo_url']);
$lien = trim($logo['link_url'] ?? ''); $lien = trim($logo['link_url'] ?? '');
if ($lien !== '') { if ($lien !== '') {
@@ -67,7 +82,7 @@ function afficherLogos($category) {
echo '<div class="logo-tile" title="' . $nom . '">'; echo '<div class="logo-tile" title="' . $nom . '">';
} }
echo '<img src="' . $image . '" alt="' . $nom . '" loading="lazy">'; afficherLogoVisuel($logo);
echo $lien !== '' ? '</a>' : '</div>'; echo $lien !== '' ? '</a>' : '</div>';
} }
} }
@@ -91,18 +106,19 @@ function afficherLogosAdmin() {
foreach ($logos as $logo) { foreach ($logos as $logo) {
$id = (int) $logo['id']; $id = (int) $logo['id'];
$nom = htmlspecialchars($logo['name']); $nom = htmlspecialchars($logo['name']);
$image = htmlspecialchars($logo['logo_url']);
$badge = $logo['category'] === 'partner' ? 'Entreprise' : 'Outil'; $badge = $logo['category'] === 'partner' ? 'Entreprise' : 'Outil';
echo '<div class="logo-admin-item">'; echo '<div class="logo-admin-item">';
echo '<div class="logo-admin-preview"><img src="' . $image . '" alt="' . $nom . '"></div>'; echo '<div class="logo-admin-preview">';
afficherLogoVisuel($logo);
echo '</div>';
echo '<div class="logo-admin-info">'; echo '<div class="logo-admin-info">';
echo '<span class="tag">' . $badge . '</span>'; echo '<span class="tag">' . $badge . '</span>';
echo '<p>' . $nom . '</p>'; echo '<p>' . $nom . '</p>';
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>';
} }
+22 -7
View File
@@ -10,15 +10,22 @@ 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
return $stmt->fetchAll(PDO::FETCH_ASSOC); 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()); die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
return []; return [];
} }
@@ -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>';
} }
+116 -2
View File
@@ -32,6 +32,41 @@ function recupererSkillParId($id) {
} }
} }
// 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) // 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) // Les items sont stockés un par ligne, au format "classes-icone|Texte" (icône optionnelle)
function afficherSkillCard($skill, $admin = false) { function afficherSkillCard($skill, $admin = false) {
@@ -53,7 +88,9 @@ function afficherSkillCard($skill, $admin = false) {
if (strpos($item, '|') !== false) { if (strpos($item, '|') !== false) {
list($itemIcone, $label) = array_map('trim', explode('|', $item, 2)); list($itemIcone, $label) = array_map('trim', explode('|', $item, 2));
} }
echo '<li><i class="' . htmlspecialchars($itemIcone) . '"></i> ' . htmlspecialchars($label) . '</li>'; echo '<li>';
afficherVisuelItem($itemIcone, 'skill-item-visual');
echo ' ' . htmlspecialchars($label) . '</li>';
} }
echo '</ul>'; echo '</ul>';
@@ -62,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>';
} }
@@ -96,3 +133,80 @@ function afficherSkillsAdmin() {
} }
echo '</div>'; 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>';
}
+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>';
+23 -9
View File
@@ -13,15 +13,22 @@ 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
return $stmt->fetchAll(PDO::FETCH_ASSOC); 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()); die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
return []; return [];
} }
@@ -32,13 +39,20 @@ 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();
return $stmt->fetch(PDO::FETCH_ASSOC); 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) { } catch (PDOException $e) {
die("Erreur de connexion à la base de donnée portfolio :" . $e->getMessage()); die("Erreur de connexion à la base de donnée portfolio :" . $e->getMessage());
// Retourner false en cas d'eerreur // Retourner false en cas d'eerreur
@@ -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');
+104 -12
View File
@@ -52,6 +52,16 @@ $noticeTitle = recupererSetting('site_notice_title') ?? '';
$noticeText = recupererSetting('site_notice_text') ?? ''; $noticeText = recupererSetting('site_notice_text') ?? '';
$noticeEnabled = recupererSetting('site_notice_enabled') ?? '1'; $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 // Page du panneau d'administration
?> ?>
@@ -70,7 +80,9 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<a href="#admin-ecoles"><i class="fa-solid fa-graduation-cap"></i> Écoles</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-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-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-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-utilisateurs"><i class="fa-solid fa-users"></i> Utilisateurs</a>
<a href="#admin-donnees"><i class="fa-solid fa-database"></i> Données</a> <a href="#admin-donnees"><i class="fa-solid fa-database"></i> Données</a>
</nav> </nav>
@@ -87,6 +99,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 +164,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 +235,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 +303,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']; ?>">
@@ -319,30 +335,39 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<!-- ========== SECTION LOGOS (bandeaux page d'accueil) ========== --> <!-- ========== SECTION LOGOS (bandeaux page d'accueil) ========== -->
<section id="admin-logos" class="admin-section"> <section id="admin-logos" class="admin-section">
<h2 class="admin-section-title"><i class="fa-solid fa-images"></i> Logos (page d'accueil)</h2> <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' : ''; ?>> <details class="admin-block" <?php echo $logoAModifier ? 'open' : ''; ?>>
<summary><?php echo $logoAModifier ? 'Modifier le logo : ' . htmlspecialchars($logoAModifier['name']) : '+ Nouveau logo'; ?></summary> <summary><?php echo $logoAModifier ? 'Modifier le logo : ' . htmlspecialchars($logoAModifier['name']) : '+ Nouvelle entreprise'; ?></summary>
<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']; ?>">
<?php endif; ?> <?php endif; ?>
<label>Bandeau *</label> <!-- Seul le bandeau "Ils m'ont fait confiance" est géré ici -->
<select name="category"> <input type="hidden" name="category" value="partner">
<option value="tool" <?php echo (!$logoAModifier || $logoAModifier['category'] === 'tool') ? 'selected' : ''; ?>>Outils que je sais manipuler</option>
<option value="partner" <?php echo ($logoAModifier && $logoAModifier['category'] === 'partner') ? 'selected' : ''; ?>>Ils m'ont fait confiance</option>
</select>
<label>Nom (outil ou entreprise) *</label> <label>Nom de l'entreprise *</label>
<input type="text" name="name" required placeholder="Docker, Proxmox, Nom de l'entreprise..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['name']) : ''; ?>"> <input type="text" name="name" required placeholder="Nom de l'entreprise..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['name']) : ''; ?>">
<label>Lien du logo *</label> <label>
<input type="url" name="logourl" placeholder="https://..." required value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['logo_url']) : ''; ?>"> 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> <label>Lien externe (optionnel, rend le logo cliquable)</label>
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['link_url'] ?? '') : ''; ?>"> <input type="url" name="linkurl" placeholder="https://..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['link_url'] ?? '') : ''; ?>">
@@ -360,6 +385,45 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
<?php afficherLogosAdmin(); ?> <?php afficherLogosAdmin(); ?>
</section> </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 NOTIFICATION ========== -->
<section id="admin-notification" class="admin-section"> <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> <h2 class="admin-section-title"><i class="fa-solid fa-bell"></i> Notification de la page d'accueil</h2>
@@ -368,6 +432,8 @@ $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()); ?>">
<input type="hidden" name="settings_group" value="notice">
<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); ?>">
@@ -388,6 +454,30 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
</div> </div>
</section> </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 UTILISATEURS ========== -->
<section id="admin-utilisateurs" class="admin-section"> <section id="admin-utilisateurs" class="admin-section">
<h2 class="admin-section-title"><i class="fa-solid fa-users"></i> Utilisateurs</h2> <h2 class="admin-section-title"><i class="fa-solid fa-users"></i> Utilisateurs</h2>
@@ -398,6 +488,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>
@@ -432,7 +523,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>
@@ -446,6 +537,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>
+17 -3
View File
@@ -6,6 +6,16 @@ $pageTitle = 'Contact';
// On ajoute ici le header // On ajoute ici le header
require_once("../elements/header.php"); 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';
?> ?>
<div class="contact-page"> <div class="contact-page">
@@ -14,9 +24,13 @@ require_once("../elements/header.php");
<div class="contact-info"> <div class="contact-info">
<p class="contact-title">Contact</p> <p class="contact-title">Contact</p>
<p class="contact-name">Esteban MOUSSU</p> <p class="contact-name">Esteban MOUSSU</p>
<p class="contact-line"><i class="fa-solid fa-envelope"></i> esteban.moussu@ik.me</p> <?php if ($contactEmailEnabled && !empty($contactEmail)): ?>
<p class="contact-line"><i class="fa-solid fa-location-dot"></i> 20 Grande rue</p> <p class="contact-line"><i class="fa-solid fa-envelope"></i> <?php echo htmlspecialchars($contactEmail); ?></p>
<p class="contact-line">52500 Broncourt</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> </div>
<!-- Formulaire d'envoi --> <!-- Formulaire d'envoi -->
+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);
+35 -5
View File
@@ -12,10 +12,19 @@ require_once('./../elements/header.php');
// La notification est maintenant dynamique : modifiable depuis le panel Admin (table settings) // La notification est maintenant dynamique : modifiable depuis le panel Admin (table settings)
require_once('../functions/listSettings.php'); require_once('../functions/listSettings.php');
// Bandeaux de logos ("Outils que je sais manipuler" / "Ils m'ont fait confiance"), // Bandeau "Ils m'ont fait confiance", géré depuis le panel Admin (table logo_banners)
// gérés depuis le panel Admin (table logo_banners)
require_once('../functions/listLogos.php'); 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) // Affichage de la notification (si elle est activée dans le panel Admin)
afficherNotice(); afficherNotice();
?> ?>
@@ -39,13 +48,34 @@ afficherNotice();
</a> </a>
</section> </section>
<?php if (!empty(listeLogos('tool'))): ?> <?php
<!-- Bandeau défilant des outils maîtrisés, géré depuis le panel Admin --> // 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"> <section class="logo-banner-section">
<div class="logo-banner-header"> <div class="logo-banner-header">
<h2>Outils que je sais manipuler</h2> <h2>Outils que je sais manipuler</h2>
</div> </div>
<?php afficherLogos('tool'); ?> <?php afficherOutilsCompetences(); ?>
</section> </section>
<?php endif; ?> <?php endif; ?>
+12 -6
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') {
@@ -16,21 +19,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// La catégorie est restreinte à 'tool' ou 'partner' (sinon on retombe sur 'tool') // La catégorie est restreinte à 'tool' ou 'partner' (sinon on retombe sur 'tool')
$category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool'; $category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool';
$name = trim($_POST['name'] ?? ''); $name = trim($_POST['name'] ?? '');
$icon = trim($_POST['icon'] ?? '');
$logo_url = trim($_POST['logourl'] ?? ''); $logo_url = trim($_POST['logourl'] ?? '');
$link_url = trim($_POST['linkurl'] ?? ''); $link_url = trim($_POST['linkurl'] ?? '');
$position = (int) ($_POST['position'] ?? 0); $position = (int) ($_POST['position'] ?? 0);
// On vérifie que les champs obligatoires ne sont pas vide // On vérifie que les champs obligatoires ne sont pas vides : il faut un nom, et
if (!empty($name) && !empty($logo_url)) { // au moins l'un des deux visuels (icône Devicon ou image)
if (!empty($name) && (!empty($icon) || !empty($logo_url))) {
try { try {
// On prépare la requête SQL pour insérer le nouveau logo // On prépare la requête SQL pour insérer le nouveau logo
$stmt = $dbh->prepare('INSERT INTO logo_banners (category, name, logo_url, link_url, position) VALUES (:category, :name, :logo_url, :link_url, :position)'); $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 // On execute la requete préparée
$stmt->execute([ $stmt->execute([
':category' => $category, ':category' => $category,
':name' => $name, ':name' => $name,
':logo_url' => $logo_url, ':icon' => $icon !== '' ? $icon : null,
':logo_url' => $logo_url !== '' ? $logo_url : null,
':link_url' => $link_url, ':link_url' => $link_url,
':position' => $position ':position' => $position
]); ]);
@@ -48,7 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit; exit;
} }
} else { } else {
echo "Veuillez remplir tous les champs obligatoires."; echo "Veuillez remplir le nom et au moins une icône ou une image.";
exit; exit;
} }
} else { } else {
+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');
+6 -1
View File
@@ -1,6 +1,11 @@
<?php <?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'); header('Location: ../captcha.php');
exit; exit;
} }
+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
+10 -5
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,10 +12,13 @@ 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']) ||
empty($_POST['logourl']) (empty($_POST['icon']) && empty($_POST['logourl']))
) { ) {
header('Location: ../pages/admin.php'); header('Location: ../pages/admin.php');
exit; exit;
@@ -25,19 +28,21 @@ if ( // Si les champs sont vides on renvoie vers admin.php
$id = (int) $_POST['id']; $id = (int) $_POST['id'];
$category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool'; $category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool';
$name = trim($_POST['name']); $name = trim($_POST['name']);
$logourl = trim($_POST['logourl']); $icon = trim($_POST['icon'] ?? '');
$logourl = trim($_POST['logourl'] ?? '');
$linkurl = trim($_POST['linkurl'] ?? ''); $linkurl = trim($_POST['linkurl'] ?? '');
$position = (int) ($_POST['position'] ?? 0); $position = (int) ($_POST['position'] ?? 0);
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
$stmt = $dbh->prepare(' $stmt = $dbh->prepare('
UPDATE logo_banners UPDATE logo_banners
SET category = :category, name = :name, logo_url = :logo_url, link_url = :link_url, position = :position SET category = :category, name = :name, icon = :icon, logo_url = :logo_url, link_url = :link_url, position = :position
WHERE id = :id WHERE id = :id
'); ');
$stmt->bindValue(':category', $category, PDO::PARAM_STR); $stmt->bindValue(':category', $category, PDO::PARAM_STR);
$stmt->bindValue(':name', $name, PDO::PARAM_STR); $stmt->bindValue(':name', $name, PDO::PARAM_STR);
$stmt->bindValue(':logo_url', $logourl, 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(':link_url', $linkurl, PDO::PARAM_STR);
$stmt->bindValue(':position', $position, PDO::PARAM_INT); $stmt->bindValue(':position', $position, PDO::PARAM_INT);
$stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->bindValue(':id', $id, PDO::PARAM_INT);
+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());
} }
+33 -7
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');
@@ -20,11 +23,29 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
// On récupère les données pour les mettres en variables locales // 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 // La checkbox n'est envoyée que si elle est cochée
$reglages = [ // settings_group : évite qu'un formulaire écrase les réglages d'un autre (ex : la case
'site_notice_title' => trim($_POST['notice_title'] ?? ''), // "notice_enabled" non cochée ne doit pas désactiver le captcha, et inversement)
'site_notice_text' => trim($_POST['notice_text'] ?? ''), $groupeReglages = $_POST['settings_group'] ?? 'notice';
'site_notice_enabled' => isset($_POST['notice_enabled']) ? '1' : '0'
]; 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 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 // ON DUPLICATE KEY UPDATE : crée le réglage s'il n'existe pas encore, le met à jour sinon
@@ -41,7 +62,12 @@ try { // On essaie d'executer une requête qui est préparée pour chaque régla
} }
// Log Discord // Log Discord
sendLog("Modification de la notification de la page d'accueil"); $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 // On renvoie l'utilisateur sur admin.php
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);
+96 -8
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;
@@ -1349,8 +1387,10 @@ img {
} }
} }
/* Pas de fond/bordure : les logos flottent directement sur le fond de la page, /* Carte derrière chaque logo, avec un fond SOMBRE FIXE (pas var(--surface), qui devient
comme un bandeau "trusted by" classique */ blanc en thème clair) : certains logos PNG ont un fond noir non transparent intégré à
l'image, qui doit rester sur un fond sombre constant pour ne pas ressortir moche
(carré noir visible) quel que soit le thème actif sur le reste du site */
.logo-tile { .logo-tile {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
@@ -1359,17 +1399,21 @@ img {
width: 160px; width: 160px;
height: 90px; height: 90px;
padding: 18px 24px; padding: 18px 24px;
background: transparent; background: #14141c;
border: none; border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius);
text-decoration: none; text-decoration: none;
transition: transform 0.2s ease; transition: transform 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
} }
a.logo-tile:hover { a.logo-tile:hover {
transform: translateY(-3px); transform: translateY(-3px);
border-color: var(--accent-border);
background: #1c1c26;
} }
.logo-tile img { .logo-tile img,
.logo-tile i {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
object-fit: contain; object-fit: contain;
@@ -1377,17 +1421,36 @@ a.logo-tile:hover {
transition: filter 0.25s ease; transition: filter 0.25s ease;
} }
.logo-tile:hover img { /* Taille de la glyphe pour les icônes Devicon (les images gardent leur taille naturelle) */
/* Couleur fixe (pas var(--text), qui devient sombre en thème clair et rendrait
l'icône invisible sur le fond de tuile qui lui reste volontairement sombre) */
.logo-tile i {
font-size: 44px;
line-height: 1;
color: #e8e8f2;
}
.logo-tile:hover img,
.logo-tile:hover i {
filter: grayscale(0) opacity(1); filter: grayscale(0) opacity(1);
} }
/* Sur mobile le survol n'existe pas : on affiche les logos en couleur directement */ /* Sur mobile le survol n'existe pas : on affiche les logos en couleur directement */
@media (hover: none) { @media (hover: none) {
.logo-tile img { .logo-tile img,
.logo-tile i {
filter: grayscale(0) opacity(0.85); filter: grayscale(0) opacity(0.85);
} }
} }
/* Bandeau "Outils" : pas de niveaux de gris, on veut que les vrais logos des outils
soient reconnaissables immédiatement, sans avoir à survoler chaque tuile */
.logo-marquee-tools .logo-tile img,
.logo-marquee-tools .logo-tile i {
filter: none;
opacity: 1;
}
@media (max-width: 620px) { @media (max-width: 620px) {
.logo-tile { .logo-tile {
width: 130px; width: 130px;
@@ -1446,6 +1509,11 @@ a.logo-tile:hover {
object-fit: contain; object-fit: contain;
} }
.logo-admin-preview i {
font-size: 40px;
color: var(--text);
}
.logo-admin-info { .logo-admin-info {
padding: 14px 16px 4px; padding: 14px 16px 4px;
} }
@@ -1564,6 +1632,14 @@ a.logo-tile:hover {
text-align: center; text-align: center;
} }
/* Vraie image de logo (format "img:URL") pour les outils absents des polices d'icônes */
.skill-item-visual {
width: 18px;
height: 18px;
object-fit: contain;
flex-shrink: 0;
}
/* Boutons admin dans les cartes de compétences */ /* Boutons admin dans les cartes de compétences */
.skill-category .admin-actions { .skill-category .admin-actions {
margin-top: 18px; margin-top: 18px;
@@ -1955,6 +2031,18 @@ a.card-link::after {
font-size: 0.85em; font-size: 0.85em;
} }
/* Texte d'introduction sous un titre de section admin (voir Logos partenaires) */
.admin-section-intro {
max-width: 1200px;
margin: 0 auto 16px;
padding: 0 clamp(16px, 4vw, 40px);
}
.admin-section-intro a {
color: var(--accent-light);
font-weight: 600;
}
/* Bloc repliable contenant chaque formulaire */ /* Bloc repliable contenant chaque formulaire */
.admin-block { .admin-block {
max-width: 1200px; max-width: 1200px;