Compare commits
9
Commits
main
...
e5bd31fc65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5bd31fc65 | ||
|
|
a4ccd7e2a9 | ||
|
|
e26dbd388f | ||
|
|
a1c99f1de8 | ||
|
|
c04a9b5ab8 | ||
|
|
c28fae7061 | ||
|
|
91060537c8 | ||
|
|
5b76997895 | ||
|
|
4c35c727d5 |
No files matched your search
+15
@@ -0,0 +1,15 @@
|
||||
# Config Claude Code
|
||||
.claude/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Éditeurs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
+13
-10
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/functions/security.php');
|
||||
|
||||
// Ajout des logs Discord (chemin via __DIR__ car captcha.php est à la racine du site)
|
||||
require_once(__DIR__ . '/functions/sendLogs.php');
|
||||
@@ -180,16 +180,19 @@ $spamSubjects = [
|
||||
['text' => 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT', 'reason' => 'Promesse d’argent contre des données personnelles'],
|
||||
];
|
||||
|
||||
// Liste des expéditeurs spam
|
||||
// Liste des expéditeurs spam : mêmes formats d'affichage (nom, pas d'adresse brute) que les
|
||||
// expéditeurs normaux ci-dessus, pour qu'il ne soit pas possible de repérer les mails
|
||||
// frauduleux d'un simple coup d'œil sur l'expéditeur. La seule façon de les distinguer doit
|
||||
// être de lire le contenu (sujet), comme un vrai email de phishing usurpant une identité connue.
|
||||
$spamSenders = [
|
||||
'service-urgent@compte-bloque-now.biz',
|
||||
'cadeau-gratuit@super-offre-win.click',
|
||||
'banque-alerte@verification-securite.top',
|
||||
'colis-stop@paiement-obligatoire.help',
|
||||
'admin-compte@action-immediate.world',
|
||||
'support-prime@argent-rapide-fast.support',
|
||||
'verification@motdepasse-danger.win',
|
||||
'alerte@compte-fermeture.click'
|
||||
'Sécurité du compte',
|
||||
'Service client',
|
||||
'Banque familiale',
|
||||
'Livraison Colis',
|
||||
'Support technique',
|
||||
'Service facturation',
|
||||
'Assurance Habitation',
|
||||
'Mutuelle Santé'
|
||||
];
|
||||
|
||||
// Liste des aperçus normaux
|
||||
|
||||
+63
-89
@@ -1,16 +1,11 @@
|
||||
-- On créer la base de donnée si elle n'existe déjà aps
|
||||
CREATE DATABASE IF NOT EXISTS portfolio
|
||||
CREATE DATABASE IF NOT EXISTS portfolio;
|
||||
USE portfolio;
|
||||
|
||||
-- On créer un utilisateur avec un mot de passe fort qui est à changer après execution dus script (voir bitwarden)
|
||||
CREATE USER 'srv_portfolio'@'%' IDENTIFIED BY 'mdp';
|
||||
|
||||
-- On met les droits au compte sur la base portfolio à l'utilisateur
|
||||
-- Mot de passe à changer après exécution du script (voir Bitwarden)
|
||||
CREATE USER IF NOT EXISTS 'srv_portfolio'@'%' IDENTIFIED BY 'mdp';
|
||||
GRANT ALL ON portfolio.* TO 'srv_portfolio'@'%';
|
||||
|
||||
-- On actualise les permissions
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
-- Création de la table user : (Généré par Beekeeper Studio : équivalent à dbGate)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
@@ -18,63 +13,75 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
|
||||
created_at DATETIME NULL DEFAULT NULL,
|
||||
last_login DATETIME NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX idx_username (username)
|
||||
);
|
||||
|
||||
-- Création de la table projects : (Généré par Beekeeper Studio)
|
||||
INSERT IGNORE INTO users (id, created_at, last_login, password, role, username)
|
||||
VALUES (11, '2026-06-08 11:10:21', '2026-06-08 13:10:41', '$2y$12$x01fDqrHQEYIyj7MDHa72eRyl2j/n5cNq9sv3.FIiQCaRsB50Q4iK', 'admin', 'admin');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(100) NULL,
|
||||
tags VARCHAR(255) NULL,
|
||||
description TEXT NOT NULL,
|
||||
image_url VARCHAR(255) NOT NULL,
|
||||
markdown LONGTEXT NULL,
|
||||
link_url VARCHAR(255) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Création de la table projects : (Généré par Beekeeper Studio)
|
||||
CREATE TABLE IF NOT EXISTS experiences (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(100) NULL,
|
||||
xptype VARCHAR(100) NOT NULL,
|
||||
tags VARCHAR(255) NULL,
|
||||
description TEXT NOT NULL,
|
||||
duration VARCHAR(100) NOT NULL,
|
||||
image_url VARCHAR(255) NOT NULL,
|
||||
markdown LONGTEXT NULL,
|
||||
link_url VARCHAR(255) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Compte de test (PLUS A JOUR METTRE UN HASH)
|
||||
-- INSERT INTO users (username, password, role)
|
||||
-- VALUES ('admin', 'admin', 'admin');
|
||||
insert into `users` (`created_at`, `id`, `last_login`, `password`, `role`, `username`)
|
||||
values ('2026-06-08 11:10:21', 11, '2026-06-08 13:10:41', '$2y$12$x01fDqrHQEYIyj7MDHa72eRyl2j/n5cNq9sv3.FIiQCaRsB50Q4iK', 'admin', 'admin');
|
||||
CREATE TABLE IF NOT EXISTS schools (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(100) NULL,
|
||||
tags VARCHAR(255) NULL,
|
||||
description TEXT NOT NULL,
|
||||
duration VARCHAR(100) NOT NULL,
|
||||
image_url VARCHAR(255) NOT NULL,
|
||||
markdown LONGTEXT NULL,
|
||||
link_url VARCHAR(255) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Ajout d'une collonne status pour les projets et les expériences pour pouvoir différencier les projets/expériences en cours, terminés ou à venir
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN status VARCHAR(100) NULL AFTER title;
|
||||
ALTER TABLE experiences
|
||||
ADD COLUMN status VARCHAR(100) NULL AFTER title;
|
||||
|
||||
-- Table de réglages du site (clé / valeur) : utilisée pour la notification de la page d'accueil
|
||||
-- Réglages du site (clé / valeur), utilisés pour la notification de la page d'accueil et le captcha
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
name VARCHAR(50) NOT NULL,
|
||||
value TEXT NULL,
|
||||
PRIMARY KEY (name)
|
||||
);
|
||||
|
||||
-- Valeurs par défaut de la notification (modifiables ensuite depuis le panel Admin)
|
||||
INSERT INTO settings (name, value) VALUES
|
||||
('site_notice_title', 'Des bugs peuvent encore se produire.'),
|
||||
('site_notice_text', 'Si le CSS ou les images s''affichent mal, pensez à vider le cache de votre navigateur (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications.'),
|
||||
('site_notice_enabled', '1')
|
||||
('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);
|
||||
|
||||
-- Table des compétences : une ligne = une catégorie (carte sur la page Compétences)
|
||||
-- items : un item par ligne, au format "classes-icone-fontawesome|Texte" (l'icône est optionnelle : "Texte" seul fonctionne aussi)
|
||||
-- items : un item par ligne, au format "classes-icone|Texte" (icône optionnelle)
|
||||
CREATE TABLE IF NOT EXISTS skills (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
@@ -85,7 +92,6 @@ CREATE TABLE IF NOT EXISTS skills (
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Compétences actuelles (reprises du CV)
|
||||
INSERT INTO skills (title, icon, items, position) VALUES
|
||||
('Systèmes d''exploitation', 'fa-solid fa-computer', 'fa-brands fa-microsoft|Windows (Serveur/XP/8/10/11)\nfa-brands fa-linux|Linux (Debian/Ubuntu)\nfa-brands fa-apple|macOS', 1),
|
||||
('Réseaux', 'fa-solid fa-network-wired', 'fa-solid fa-location-dot|Adressage IP\nfa-solid fa-diagram-project|VLAN\nfa-solid fa-shield-halved|Pare-feu (iptables/ufw)\nfa-solid fa-globe|DNS (Bind9)\nfa-solid fa-server|DHCP (ISC DHCP)\nfa-solid fa-arrows-left-right|Proxy (Squid)', 2),
|
||||
@@ -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),
|
||||
('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
|
||||
CREATE TABLE IF NOT EXISTS schools (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(100) NULL,
|
||||
tags VARCHAR(255) NULL,
|
||||
description TEXT NOT NULL,
|
||||
duration VARCHAR(100) NOT NULL,
|
||||
image_url VARCHAR(255) NOT NULL,
|
||||
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)
|
||||
-- category : 'tool' (bandeau "Outils que je sais manipuler") ou 'partner' (bandeau "Ils m'ont fait confiance")
|
||||
-- icon : classes CSS Devicon, prioritaire sur logo_url si les deux sont renseignés (au moins un des deux requis)
|
||||
CREATE TABLE IF NOT EXISTS logo_banners (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
category ENUM('tool', 'partner') NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
logo_url VARCHAR(255) NOT NULL,
|
||||
icon VARCHAR(100) NULL,
|
||||
logo_url VARCHAR(255) NULL,
|
||||
link_url VARCHAR(255) NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Table créée vide volontairement : les logos sont ajoutés depuis le panel Admin
|
||||
-- (section Logos) pour éviter d'afficher des images devinées/potentiellement cassées
|
||||
-- PDF de projects/experiences/schools : tables associées plutôt qu'une colonne sur la table
|
||||
-- parente, pour que les pages qui LISTENT ces éléments n'aient jamais à charger le contenu
|
||||
-- binaire des PDF en mémoire (seul le nom de fichier y est utilisé)
|
||||
CREATE TABLE IF NOT EXISTS project_pdfs (
|
||||
project_id INT UNSIGNED NOT NULL,
|
||||
pdf_filename VARCHAR(255) NOT NULL,
|
||||
pdf_data LONGBLOB NOT NULL,
|
||||
PRIMARY KEY (project_id),
|
||||
CONSTRAINT fk_project_pdfs_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Juillet 2026 (3) : PDF en drag & drop pour les projets,
|
||||
-- expériences et écoles, lu depuis un mini lecteur intégré
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS experience_pdfs (
|
||||
experience_id INT UNSIGNED NOT NULL,
|
||||
pdf_filename VARCHAR(255) NOT NULL,
|
||||
pdf_data LONGBLOB NOT NULL,
|
||||
PRIMARY KEY (experience_id),
|
||||
CONSTRAINT fk_experience_pdfs_experience FOREIGN KEY (experience_id) REFERENCES experiences(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- pdf_data : contenu binaire du PDF (LONGBLOB = ~4 Go, largement suffisant)
|
||||
-- pdf_filename : nom original du fichier (affiché dans le panel admin et le lecteur)
|
||||
-- Priorité d'affichage sur la page de détails : markdown > pdf > lien externe
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN pdf_data LONGBLOB NULL,
|
||||
ADD COLUMN pdf_filename VARCHAR(255) NULL;
|
||||
|
||||
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;
|
||||
CREATE TABLE IF NOT EXISTS school_pdfs (
|
||||
school_id INT UNSIGNED NOT NULL,
|
||||
pdf_filename VARCHAR(255) NOT NULL,
|
||||
pdf_data LONGBLOB NOT NULL,
|
||||
PRIMARY KEY (school_id),
|
||||
CONSTRAINT fk_school_pdfs_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
; Config PHP pour le conteneur Docker (image php:8.2-apache)
|
||||
; Copiée dans /usr/local/etc/php/conf.d/ par le Dockerfile généré par scriptsite.sh
|
||||
|
||||
; Augmente les limites d'upload pour permettre le dépôt de PDF depuis le panel admin
|
||||
; post_max_size doit toujours être >= upload_max_filesize
|
||||
upload_max_filesize = 20M
|
||||
post_max_size = 20M
|
||||
|
||||
; Coupe l'affichage des erreurs PHP dans le HTML en production (sécurité : ne pas exposer
|
||||
; les chemins serveur aux visiteurs) et les envoie dans les logs Apache/Docker à la place.
|
||||
; Bonus : évite les warnings HTML qui provoquaient "headers already sent" une fois affichés
|
||||
; avant le session_start()/header() des scripts.
|
||||
display_errors = Off
|
||||
log_errors = On
|
||||
+2
-1
@@ -52,7 +52,8 @@ if (isset($_GET['success']) && $_GET['success'] === '1') {
|
||||
var cibles = document.querySelectorAll(
|
||||
'.project-card, .quick-link-card, .skill-category, .contact-card, ' +
|
||||
'.login-card, .project-form-card, .cgu-card, .logo-admin-item, ' +
|
||||
'.page-header, .quick-links-header, .skills-header, .projects-title, .logo-banner-header'
|
||||
'.page-header, .quick-links-header, .skills-header, .projects-title, .logo-banner-header, ' +
|
||||
'.stat-item'
|
||||
);
|
||||
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
|
||||
+29
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
|
||||
// Ajout ici d'un démarrage de session : header étant dans toutes les pages
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
|
||||
// Ajout ici la vérification du captcha
|
||||
require_once('../scripts/requireCaptcha.php');
|
||||
@@ -20,6 +20,15 @@ sendPageLog();
|
||||
// Titre de la page : chaque page peut définir $pageTitle avant d'inclure le header
|
||||
$pageTitle = isset($pageTitle) ? $pageTitle . ' — Esteban MOUSSU' : 'Esteban MOUSSU — Portfolio';
|
||||
|
||||
// Description : chaque page peut définir $pageDescription avant d'inclure le header
|
||||
// (sinon description générique, utilisée pour le SEO et les aperçus de partage)
|
||||
$pageDescription = isset($pageDescription)
|
||||
? $pageDescription
|
||||
: 'Portfolio d\'Esteban MOUSSU, étudiant en informatique passionné par les systèmes, les réseaux et le développement web. Projets, expériences et compétences.';
|
||||
|
||||
// URL absolue de la page courante (pour og:url et le lien canonique)
|
||||
$pageUrl = 'https://estebanadmin.fr' . strtok($_SERVER['REQUEST_URI'], '?');
|
||||
|
||||
// Page courante pour souligner le lien actif dans la navigation
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
?>
|
||||
@@ -31,6 +40,22 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo htmlspecialchars($pageTitle); ?></title>
|
||||
|
||||
<!-- SEO : description + lien canonique -->
|
||||
<meta name="description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||
<link rel="canonical" href="<?php echo htmlspecialchars($pageUrl); ?>">
|
||||
|
||||
<!-- Aperçu de partage (réseaux sociaux, Discord, etc.) -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="<?php echo htmlspecialchars($pageTitle); ?>">
|
||||
<meta property="og:description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||
<meta property="og:url" content="<?php echo htmlspecialchars($pageUrl); ?>">
|
||||
<meta property="og:image" content="https://estebanadmin.fr/memoji.png">
|
||||
<meta property="og:locale" content="fr_FR">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="<?php echo htmlspecialchars($pageTitle); ?>">
|
||||
<meta name="twitter:description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||
<meta name="twitter:image" content="https://estebanadmin.fr/memoji.png">
|
||||
|
||||
<!-- Favicon du site -->
|
||||
<link rel="icon" type="image/png" href="../memoji.png">
|
||||
|
||||
@@ -52,6 +77,9 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
||||
<link rel="stylesheet" href="../styles/primary_css.css">
|
||||
<!-- Ajout de Font Awesome pour les icones de home.php-->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||
<!-- Devicon : équivalent gratuit de Font Awesome dédié aux logos d'outils/langages/frameworks,
|
||||
utilisé pour le bandeau "Outils que je sais manipuler" de la page d'accueil -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/devicon@2.15.1/devicon.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
+24
-8
@@ -12,11 +12,13 @@ function listeLogos($category = null) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
// SELECT * : la colonne icon est optionnelle (le bandeau continue de fonctionner
|
||||
// avec les logos déjà en place même si la migration SQL n'a pas encore été exécutée)
|
||||
if ($category !== null) {
|
||||
$stmt = $dbh->prepare('SELECT 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);
|
||||
} 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();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -30,7 +32,7 @@ function recupererLogoParId($id) {
|
||||
global $dbh;
|
||||
|
||||
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->execute();
|
||||
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
|
||||
// N'affiche rien si la catégorie est vide (la section entière est masquée côté page d'accueil)
|
||||
function afficherLogos($category) {
|
||||
@@ -58,7 +74,6 @@ function afficherLogos($category) {
|
||||
for ($repetition = 0; $repetition < 2; $repetition++) {
|
||||
foreach ($logos as $logo) {
|
||||
$nom = htmlspecialchars($logo['name']);
|
||||
$image = htmlspecialchars($logo['logo_url']);
|
||||
$lien = trim($logo['link_url'] ?? '');
|
||||
|
||||
if ($lien !== '') {
|
||||
@@ -67,7 +82,7 @@ function afficherLogos($category) {
|
||||
echo '<div class="logo-tile" title="' . $nom . '">';
|
||||
}
|
||||
|
||||
echo '<img src="' . $image . '" alt="' . $nom . '" loading="lazy">';
|
||||
afficherLogoVisuel($logo);
|
||||
echo $lien !== '' ? '</a>' : '</div>';
|
||||
}
|
||||
}
|
||||
@@ -91,18 +106,19 @@ function afficherLogosAdmin() {
|
||||
foreach ($logos as $logo) {
|
||||
$id = (int) $logo['id'];
|
||||
$nom = htmlspecialchars($logo['name']);
|
||||
$image = htmlspecialchars($logo['logo_url']);
|
||||
$badge = $logo['category'] === 'partner' ? 'Entreprise' : 'Outil';
|
||||
|
||||
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 '<span class="tag">' . $badge . '</span>';
|
||||
echo '<p>' . $nom . '</p>';
|
||||
echo '</div>';
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_logo=' . $id . '#admin-logos">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeLogo.php?id=' . $id . '" 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>';
|
||||
}
|
||||
|
||||
@@ -10,15 +10,22 @@ function listeProjets() {
|
||||
|
||||
// Essayer
|
||||
try {
|
||||
// Requête SQL qui récupère titre, tags, description, image_url
|
||||
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
|
||||
// même si la migration SQL n'a pas encore été exécutée)
|
||||
$stmt = $dbh->prepare('SELECT * FROM projects ORDER BY id DESC');
|
||||
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||
// stocké à part dans project_pdfs (voir migration Juillet 2026 (6)), on ne récupère
|
||||
// ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
|
||||
// (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste de projets
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT p.id, p.title, p.status, p.tags, p.description, p.image_url, p.markdown, p.link_url, p.created_at,
|
||||
pp.pdf_filename
|
||||
FROM projects p
|
||||
LEFT JOIN project_pdfs pp ON pp.project_id = p.id
|
||||
ORDER BY p.id DESC
|
||||
');
|
||||
// Execution de la requête SQL
|
||||
$stmt->execute();
|
||||
// Retourne toutes les données collectés dans un ARRAY
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) { // En cas d'erreur
|
||||
} catch (PDOException $e) { // En cas d'erreur
|
||||
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
||||
return [];
|
||||
}
|
||||
@@ -29,7 +36,15 @@ function recupererProjetParId($id) {
|
||||
global $dbh;
|
||||
|
||||
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->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
@@ -164,7 +179,7 @@ function afficherProjetsAdmin() {
|
||||
// Boutons qui vont permettrent la modification / Suppresion
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '#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>';
|
||||
|
||||
@@ -10,9 +10,17 @@ function listeEcoles() {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
|
||||
// même si la migration SQL n'a pas encore été exécutée)
|
||||
$stmt = $dbh->prepare('SELECT * FROM schools ORDER BY id DESC');
|
||||
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||
// stocké à part dans school_pdfs (voir migration Juillet 2026 (6)), on ne récupère
|
||||
// ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
|
||||
// (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste d'écoles
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT s.id, s.title, s.status, s.tags, s.description, s.duration, s.image_url, s.markdown, s.link_url, s.created_at,
|
||||
sp.pdf_filename
|
||||
FROM schools s
|
||||
LEFT JOIN school_pdfs sp ON sp.school_id = s.id
|
||||
ORDER BY s.id DESC
|
||||
');
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
@@ -25,7 +33,15 @@ function recupererEcoleParId($id) {
|
||||
global $dbh;
|
||||
|
||||
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->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
@@ -98,7 +114,7 @@ function afficherEcoleCard($ecole, $admin = false) {
|
||||
if ($admin) {
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_school=' . $id . '#admin-ecoles">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeSchool.php?id=' . $id . '" 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>';
|
||||
}
|
||||
|
||||
|
||||
+116
-2
@@ -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)
|
||||
// Les items sont stockés un par ligne, au format "classes-icone|Texte" (icône optionnelle)
|
||||
function afficherSkillCard($skill, $admin = false) {
|
||||
@@ -53,7 +88,9 @@ function afficherSkillCard($skill, $admin = false) {
|
||||
if (strpos($item, '|') !== false) {
|
||||
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>';
|
||||
@@ -62,7 +99,7 @@ function afficherSkillCard($skill, $admin = false) {
|
||||
if ($admin) {
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_skill=' . $id . '#admin-competences">Modifier</a>';
|
||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeSkill.php?id=' . $id . '" 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>';
|
||||
}
|
||||
|
||||
@@ -96,3 +133,80 @@ function afficherSkillsAdmin() {
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
// Items exclus du bandeau de la home au cas par cas, en plus du filtre automatique
|
||||
// (icône Devicon "réelle" mais jugée pas assez reconnaissable seule, sans le texte à côté)
|
||||
const OUTILS_EXCLUS_DU_BANDEAU = ['Bash'];
|
||||
|
||||
// Fonction qui récupère tous les items de compétences représentant un vrai outil identifiable
|
||||
// (icône Devicon ou image "img:"), toutes catégories confondues. Les items en texte seul
|
||||
// SONT EXCLUS, ainsi que les icônes Font Awesome génériques (fa-solid/fa-brands) : dans un
|
||||
// bandeau qui n'affiche pas le texte à côté (juste une info-bulle au survol), une icône
|
||||
// générique comme un pictogramme "carte" pour "Adressage IP" ou "carnet de contact" pour
|
||||
// "LDAP" ne représente aucun outil reconnaissable — ça reste illisible pour un visiteur qui
|
||||
// ne survole pas. Ces items restent affichés normalement sur la page Compétences (avec leur
|
||||
// texte à côté, donc sans ambiguïté), seul le bandeau de la home les filtre.
|
||||
function listeIconesCompetences() {
|
||||
$categories = listeSkills();
|
||||
$resultats = [];
|
||||
|
||||
foreach ($categories as $categorie) {
|
||||
$items = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $categorie['items'])));
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (strpos($item, '|') === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
list($icone, $label) = array_map('trim', explode('|', $item, 2));
|
||||
if ($icone === '' || $label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seuls les vrais logos (Devicon ou image) sont assez reconnaissables sans texte
|
||||
$estUnVraiLogo = strpos($icone, 'devicon-') === 0 || strpos($icone, 'img:') === 0;
|
||||
if (!$estUnVraiLogo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($label, OUTILS_EXCLUS_DU_BANDEAU, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resultats[] = ['icon' => $icone, 'label' => $label];
|
||||
}
|
||||
}
|
||||
|
||||
return $resultats;
|
||||
}
|
||||
|
||||
// Fonction qui affiche le bandeau défilant des outils, directement à partir des icônes
|
||||
// de la page Compétences (voir listeIconesCompetences). Réutilise les mêmes classes CSS
|
||||
// que les bandeaux de logo_banners pour un rendu visuel identique (voir primary_css.css).
|
||||
// "logo-marquee-tools" désactive le filtre grayscale/survol : contrairement aux logos
|
||||
// d'entreprises, on veut que les vrais outils soient reconnaissables immédiatement.
|
||||
function afficherOutilsCompetences() {
|
||||
$outils = listeIconesCompetences();
|
||||
|
||||
if (empty($outils)) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="logo-marquee logo-marquee-tools">';
|
||||
echo '<div class="logo-track">';
|
||||
|
||||
// On répète deux fois la liste : le défilement CSS boucle sur -50%, ce qui donne
|
||||
// un effet de scroll infini fluide et sans coupure
|
||||
for ($repetition = 0; $repetition < 2; $repetition++) {
|
||||
foreach ($outils as $outil) {
|
||||
$label = htmlspecialchars($outil['label']);
|
||||
|
||||
echo '<div class="logo-tile" title="' . $label . '">';
|
||||
afficherVisuelItem($outil['icon'], '', true);
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Fonction qui récupère les chiffres clés affichés sur la home (bandeau de stats)
|
||||
// Chaque compteur retombe sur 0 en cas d'erreur (table pas encore créée...) plutôt
|
||||
// que de bloquer la page d'accueil
|
||||
function recupererStatsHome() {
|
||||
global $dbh;
|
||||
|
||||
$stats = ['projets' => 0, 'experiences' => 0];
|
||||
|
||||
try {
|
||||
$stats['projets'] = (int) $dbh->query('SELECT COUNT(*) FROM projects')->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
// Table pas encore créée : on laisse le compteur à 0
|
||||
}
|
||||
|
||||
try {
|
||||
$stats['experiences'] = (int) $dbh->query('SELECT COUNT(*) FROM experiences')->fetchColumn();
|
||||
} catch (PDOException $e) {
|
||||
// Table pas encore créée : on laisse le compteur à 0
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function afficherUsersAdmin()
|
||||
echo '<p><strong>Créé le :</strong> ' . htmlspecialchars($user["created_at"]) . '</p>';
|
||||
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a href="../scripts/removeUser.php?id=' . $user["id"] . '" class="btn-submit btn-delete" onclick="return confirm(\'Supprimer cet utilisateur ?\')">Supprimer</a>';
|
||||
echo '<a href="../scripts/removeUser.php?id=' . $user["id"] . '&csrf_token=' . urlencode(csrfToken()) . '" class="btn-submit btn-delete" onclick="return confirm(\'Supprimer cet utilisateur ?\')">Supprimer</a>';
|
||||
echo '</div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
+23
-9
@@ -13,15 +13,22 @@ function listeXP() {
|
||||
|
||||
// Essayer
|
||||
try {
|
||||
// Requête SQL qui récupère titre, type, tags, description, durée, image_url
|
||||
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
|
||||
// même si la migration SQL n'a pas encore été exécutée)
|
||||
$stmt = $dbh->prepare('SELECT * FROM experiences ORDER BY id DESC');
|
||||
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||
// stocké à part dans experience_pdfs (voir migration Juillet 2026 (6)), on ne
|
||||
// récupère ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu
|
||||
// binaire (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste d'expériences
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT e.id, e.title, e.status, e.xptype, e.tags, e.description, e.duration, e.image_url, e.markdown, e.link_url, e.created_at,
|
||||
ep.pdf_filename
|
||||
FROM experiences e
|
||||
LEFT JOIN experience_pdfs ep ON ep.experience_id = e.id
|
||||
ORDER BY e.id DESC
|
||||
');
|
||||
// Execution de la requête SQL
|
||||
$stmt->execute();
|
||||
// Retourne toutes les données collectés dans un ARRAY
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) { // En cas d'erreur
|
||||
} catch (PDOException $e) { // En cas d'erreur
|
||||
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
||||
return [];
|
||||
}
|
||||
@@ -32,13 +39,20 @@ function recupererXPParId($id) {
|
||||
global $dbh;
|
||||
|
||||
try {
|
||||
// Préparation de la requête préparée
|
||||
$stmt = $dbh->prepare('SELECT * FROM experiences WHERE id = :id');
|
||||
// Même principe que listeXP() : le PDF est joint pour son nom de fichier
|
||||
// uniquement, jamais son contenu binaire
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT e.id, e.title, e.status, e.xptype, e.tags, e.description, e.duration, e.image_url, e.markdown, e.link_url, e.created_at,
|
||||
ep.pdf_filename
|
||||
FROM experiences e
|
||||
LEFT JOIN experience_pdfs ep ON ep.experience_id = e.id
|
||||
WHERE e.id = :id
|
||||
');
|
||||
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
// En cas d'erreur => renvoyer un message avec l'erreur
|
||||
// En cas d'erreur => renvoyer un message avec l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur de connexion à la base de donnée portfolio :" . $e->getMessage());
|
||||
// Retourner false en cas d'eerreur
|
||||
@@ -195,7 +209,7 @@ function afficherXPAdmin() {
|
||||
// Boutons qui vont permettrent la modification / Suppresion
|
||||
echo '<div class="admin-actions">';
|
||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '#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>';
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
// Le header n'y étant pas je rajoute ici un démarrage de session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/functions/security.php');
|
||||
|
||||
// Redirection de l'utilisateur vers une page Home lorsqu'il arrive sur index.php
|
||||
header('location: ./pages/home.php');
|
||||
|
||||
+104
-12
@@ -52,6 +52,16 @@ $noticeTitle = recupererSetting('site_notice_title') ?? '';
|
||||
$noticeText = recupererSetting('site_notice_text') ?? '';
|
||||
$noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
|
||||
// Valeur actuelle du captcha anti-robot (activé par défaut)
|
||||
$captchaEnabled = recupererSetting('captcha_enabled') ?? '1';
|
||||
|
||||
// Valeurs actuelles des coordonnées de la page contact
|
||||
$contactEmail = recupererSetting('contact_email') ?? 'esteban.moussu@ik.me';
|
||||
$contactEmailEnabled = recupererSetting('contact_email_enabled') ?? '1';
|
||||
$contactAddressLine1 = recupererSetting('contact_address_line1') ?? '20 Grande rue';
|
||||
$contactAddressLine2 = recupererSetting('contact_address_line2') ?? '52500 Broncourt';
|
||||
$contactAddressEnabled = recupererSetting('contact_address_enabled') ?? '1';
|
||||
|
||||
// Page du panneau d'administration
|
||||
?>
|
||||
|
||||
@@ -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-competences"><i class="fa-solid fa-code"></i> Compétences</a>
|
||||
<a href="#admin-logos"><i class="fa-solid fa-images"></i> Logos</a>
|
||||
<a href="#admin-contact"><i class="fa-solid fa-address-card"></i> Contact</a>
|
||||
<a href="#admin-notification"><i class="fa-solid fa-bell"></i> Notification</a>
|
||||
<a href="#admin-securite"><i class="fa-solid fa-shield-halved"></i> Sécurité</a>
|
||||
<a href="#admin-utilisateurs"><i class="fa-solid fa-users"></i> Utilisateurs</a>
|
||||
<a href="#admin-donnees"><i class="fa-solid fa-database"></i> Données</a>
|
||||
</nav>
|
||||
@@ -87,6 +99,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<!-- enctype multipart/form-data : nécessaire pour l'upload du PDF -->
|
||||
<!-- en envoie les données à addProject.php -->
|
||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $projetAModifier ? '../scripts/updateProject.php' : '../scripts/addProject.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($projetAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
|
||||
@@ -151,6 +164,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<div class="project-form-card">
|
||||
<!-- en envoie les données à addXP.php -->
|
||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $xpAModifier ? '../scripts/updateXP.php' : '../scripts/addXP.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($xpAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
|
||||
@@ -221,6 +235,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à addSchool.php -->
|
||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $ecoleAModifier ? '../scripts/updateSchool.php' : '../scripts/addSchool.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($ecoleAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $ecoleAModifier['id']; ?>">
|
||||
@@ -288,6 +303,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à addSkill.php -->
|
||||
<form method="POST" action="<?php echo $skillAModifier ? '../scripts/updateSkill.php' : '../scripts/addSkill.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($skillAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $skillAModifier['id']; ?>">
|
||||
@@ -319,30 +335,39 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
|
||||
<!-- ========== SECTION LOGOS (bandeaux page d'accueil) ========== -->
|
||||
<section id="admin-logos" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-images"></i> Logos (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' : ''; ?>>
|
||||
<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">
|
||||
<!-- on envoie les données à addLogo.php -->
|
||||
<form method="POST" action="<?php echo $logoAModifier ? '../scripts/updateLogo.php' : '../scripts/addLogo.php'; ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
|
||||
<?php if ($logoAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $logoAModifier['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<label>Bandeau *</label>
|
||||
<select name="category">
|
||||
<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>
|
||||
<!-- Seul le bandeau "Ils m'ont fait confiance" est géré ici -->
|
||||
<input type="hidden" name="category" value="partner">
|
||||
|
||||
<label>Nom (outil ou entreprise) *</label>
|
||||
<input type="text" name="name" required placeholder="Docker, Proxmox, Nom de l'entreprise..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['name']) : ''; ?>">
|
||||
<label>Nom de l'entreprise *</label>
|
||||
<input type="text" name="name" required placeholder="Nom de l'entreprise..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['name']) : ''; ?>">
|
||||
|
||||
<label>Lien du logo *</label>
|
||||
<input type="url" name="logourl" placeholder="https://..." required value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['logo_url']) : ''; ?>">
|
||||
<label>
|
||||
Icône Devicon (facultatif, rarement utile pour une entreprise — parcourir sur
|
||||
<a href="https://devicon.dev" target="_blank" rel="noopener noreferrer">devicon.dev</a>)
|
||||
</label>
|
||||
<input type="text" name="icon" placeholder="devicon-docker-plain colored" value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['icon'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Lien de l'image (utilisé si pas d'icône)</label>
|
||||
<input type="url" name="logourl" placeholder="https://..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['logo_url'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Lien externe (optionnel, rend le logo cliquable)</label>
|
||||
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['link_url'] ?? '') : ''; ?>">
|
||||
@@ -360,6 +385,45 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<?php afficherLogosAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION CONTACT ========== -->
|
||||
<section id="admin-contact" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-address-card"></i> Coordonnées de contact</h2>
|
||||
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="hidden" name="settings_group" value="contact">
|
||||
|
||||
<label>Adresse email</label>
|
||||
<input type="email" name="contact_email" value="<?php echo htmlspecialchars($contactEmail); ?>">
|
||||
|
||||
<!-- Checkbox pour afficher / masquer l'email sur la page contact -->
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="contact_email_enabled" value="1" <?php echo $contactEmailEnabled === '1' ? 'checked' : ''; ?>>
|
||||
Afficher l'email sur la page contact
|
||||
</label>
|
||||
|
||||
<label>Adresse postale - ligne 1 (rue)</label>
|
||||
<input type="text" name="contact_address_line1" value="<?php echo htmlspecialchars($contactAddressLine1); ?>">
|
||||
|
||||
<label>Adresse postale - ligne 2 (code postal / ville)</label>
|
||||
<input type="text" name="contact_address_line2" value="<?php echo htmlspecialchars($contactAddressLine2); ?>">
|
||||
|
||||
<!-- Checkbox pour afficher / masquer l'adresse postale sur la page contact -->
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="contact_address_enabled" value="1" <?php echo $contactAddressEnabled === '1' ? 'checked' : ''; ?>>
|
||||
Afficher l'adresse postale sur la page contact
|
||||
</label>
|
||||
|
||||
<input type="submit" name="submitsettings" value="Enregistrer les coordonnées" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION NOTIFICATION ========== -->
|
||||
<section id="admin-notification" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-bell"></i> Notification de la page d'accueil</h2>
|
||||
@@ -368,6 +432,8 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="hidden" name="settings_group" value="notice">
|
||||
|
||||
<label>Titre (affiché en gras)</label>
|
||||
<input type="text" name="notice_title" value="<?php echo htmlspecialchars($noticeTitle); ?>">
|
||||
@@ -388,6 +454,30 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION SÉCURITÉ ========== -->
|
||||
<section id="admin-securite" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-shield-halved"></i> Sécurité</h2>
|
||||
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="hidden" name="settings_group" value="captcha">
|
||||
|
||||
<!-- Checkbox pour activer / désactiver le test anti-robot à l'entrée du site -->
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="captcha_enabled" value="1" <?php echo $captchaEnabled === '1' ? 'checked' : ''; ?>>
|
||||
Activer le test anti-robot (captcha) à l'entrée du site
|
||||
</label>
|
||||
|
||||
<input type="submit" name="submitsettings" value="Enregistrer" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION UTILISATEURS ========== -->
|
||||
<section id="admin-utilisateurs" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-users"></i> Utilisateurs</h2>
|
||||
@@ -398,6 +488,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<div class="project-form-card">
|
||||
<!-- en envoie les données addUser.php -->
|
||||
<form method="POST" action="../scripts/addUser.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<label>Nom du compte</label>
|
||||
<input type="text" name="user" required>
|
||||
|
||||
@@ -432,7 +523,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
Télécharge une sauvegarde JSON de toutes les données du site
|
||||
(projets, expériences, écoles, compétences, notification, utilisateurs).
|
||||
</p>
|
||||
<a class="btn-submit admin-export-btn" href="../scripts/exportData.php">
|
||||
<a class="btn-submit admin-export-btn" href="../scripts/exportData.php?csrf_token=<?php echo urlencode(csrfToken()); ?>">
|
||||
<i class="fa-solid fa-download"></i> Exporter les données (JSON)
|
||||
</a>
|
||||
|
||||
@@ -446,6 +537,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
</p>
|
||||
<form method="POST" action="../scripts/importData.php" enctype="multipart/form-data"
|
||||
onsubmit="return confirm('ATTENTION : les données actuelles du site vont être écrasées par celles du fichier. Continuer ?');">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="file" name="jsonfile" accept=".json,application/json" required>
|
||||
<input type="submit" value="Importer les données" class="btn-submit btn-delete">
|
||||
</form>
|
||||
|
||||
+17
-3
@@ -6,6 +6,16 @@ $pageTitle = 'Contact';
|
||||
|
||||
// On ajoute ici le header
|
||||
require_once("../elements/header.php");
|
||||
|
||||
// Réglages de l'email et de l'adresse (modifiables et masquables depuis l'admin)
|
||||
require_once('../functions/listSettings.php');
|
||||
|
||||
$contactEmail = recupererSetting('contact_email') ?? 'esteban.moussu@ik.me';
|
||||
$contactEmailEnabled = (recupererSetting('contact_email_enabled') ?? '1') === '1';
|
||||
|
||||
$contactAddressLine1 = recupererSetting('contact_address_line1') ?? '20 Grande rue';
|
||||
$contactAddressLine2 = recupererSetting('contact_address_line2') ?? '52500 Broncourt';
|
||||
$contactAddressEnabled = (recupererSetting('contact_address_enabled') ?? '1') === '1';
|
||||
?>
|
||||
|
||||
<div class="contact-page">
|
||||
@@ -14,9 +24,13 @@ require_once("../elements/header.php");
|
||||
<div class="contact-info">
|
||||
<p class="contact-title">Contact</p>
|
||||
<p class="contact-name">Esteban MOUSSU</p>
|
||||
<p class="contact-line"><i class="fa-solid fa-envelope"></i> esteban.moussu@ik.me</p>
|
||||
<p class="contact-line"><i class="fa-solid fa-location-dot"></i> 20 Grande rue</p>
|
||||
<p class="contact-line">52500 Broncourt</p>
|
||||
<?php if ($contactEmailEnabled && !empty($contactEmail)): ?>
|
||||
<p class="contact-line"><i class="fa-solid fa-envelope"></i> <?php echo htmlspecialchars($contactEmail); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($contactAddressEnabled && (!empty($contactAddressLine1) || !empty($contactAddressLine2))): ?>
|
||||
<p class="contact-line"><i class="fa-solid fa-location-dot"></i> <?php echo htmlspecialchars($contactAddressLine1); ?></p>
|
||||
<p class="contact-line"><?php echo htmlspecialchars($contactAddressLine2); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'envoi -->
|
||||
|
||||
+14
-7
@@ -8,10 +8,12 @@
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Types autorisés : liste blanche pour empêcher toute injection de nom de table
|
||||
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6) : on la
|
||||
// joint juste pour le nom de fichier, jamais pour son contenu binaire)
|
||||
$types = [
|
||||
'projet' => ['table' => 'projects', 'retour' => './projects.php', 'label' => 'Projet'],
|
||||
'experience' => ['table' => 'experiences', 'retour' => './xp.php', 'label' => 'Expérience'],
|
||||
'ecole' => ['table' => 'schools', 'retour' => './xp.php', 'label' => 'École'],
|
||||
'projet' => ['table' => 'projects', 'pdf' => 'project_pdfs', 'pdf_colonne' => 'project_id', 'retour' => './projects.php', 'label' => 'Projet'],
|
||||
'experience' => ['table' => 'experiences', 'pdf' => 'experience_pdfs', 'pdf_colonne' => 'experience_id', 'retour' => './xp.php', 'label' => 'Expérience'],
|
||||
'ecole' => ['table' => 'schools', 'pdf' => 'school_pdfs', 'pdf_colonne' => 'school_id', 'retour' => './xp.php', 'label' => 'École'],
|
||||
];
|
||||
|
||||
$type = $_GET['type'] ?? '';
|
||||
@@ -28,15 +30,20 @@ $infos = $types[$type];
|
||||
// Récupération de l'élément demandé
|
||||
// (pdf_data n'est volontairement pas sélectionné : le PDF est servi à part par viewPdf.php
|
||||
// dans une <iframe>, inutile de charger son contenu binaire en mémoire ici)
|
||||
$colonnesCommunes = '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 = [
|
||||
'projet' => $colonnesCommunes,
|
||||
'experience' => $colonnesCommunes . ', xptype, duration',
|
||||
'ecole' => $colonnesCommunes . ', duration',
|
||||
'experience' => $colonnesCommunes . ', e.xptype, e.duration',
|
||||
'ecole' => $colonnesCommunes . ', e.duration',
|
||||
];
|
||||
|
||||
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->execute();
|
||||
$element = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
+35
-5
@@ -12,10 +12,19 @@ require_once('./../elements/header.php');
|
||||
// La notification est maintenant dynamique : modifiable depuis le panel Admin (table settings)
|
||||
require_once('../functions/listSettings.php');
|
||||
|
||||
// Bandeaux de logos ("Outils que je sais manipuler" / "Ils m'ont fait confiance"),
|
||||
// gérés depuis le panel Admin (table logo_banners)
|
||||
// Bandeau "Ils m'ont fait confiance", géré depuis le panel Admin (table logo_banners)
|
||||
require_once('../functions/listLogos.php');
|
||||
|
||||
// Bandeau "Outils que je sais manipuler" : dérivé directement des icônes de la page
|
||||
// Compétences, pas de saisie séparée (voir listeIconesCompetences)
|
||||
require_once('../functions/listSkills.php');
|
||||
|
||||
// Chiffres clés (bandeau de stats sous le hero) : nombre réel de projets/expériences
|
||||
// en BDD, et nombre de technologies reconnaissables (réutilise listeIconesCompetences)
|
||||
require_once('../functions/listStats.php');
|
||||
$statsHome = recupererStatsHome();
|
||||
$statsHome['technologies'] = count(listeIconesCompetences());
|
||||
|
||||
// Affichage de la notification (si elle est activée dans le panel Admin)
|
||||
afficherNotice();
|
||||
?>
|
||||
@@ -39,13 +48,34 @@ afficherNotice();
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<?php if (!empty(listeLogos('tool'))): ?>
|
||||
<!-- Bandeau défilant des outils maîtrisés, géré depuis le panel Admin -->
|
||||
<?php
|
||||
// Chiffres clés : n'affiche que les compteurs non nuls (pas de "0 projets" décevant
|
||||
// si le site vient d'être initialisé)
|
||||
$statsAffichables = array_filter([
|
||||
'projets' => ['valeur' => $statsHome['projets'], 'label' => 'Projet' . ($statsHome['projets'] > 1 ? 's' : '') . ' réalisés'],
|
||||
'experiences' => ['valeur' => $statsHome['experiences'], 'label' => 'Expérience' . ($statsHome['experiences'] > 1 ? 's' : '') . ' pro'],
|
||||
'technologies' => ['valeur' => $statsHome['technologies'], 'label' => 'Technologies maîtrisées'],
|
||||
], function ($stat) { return $stat['valeur'] > 0; });
|
||||
?>
|
||||
<?php if (!empty($statsAffichables)): ?>
|
||||
<!-- Bandeau de chiffres clés : compteurs réels, calculés depuis la BDD -->
|
||||
<section class="stats-strip">
|
||||
<?php foreach ($statsAffichables as $stat): ?>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><?php echo (int) $stat['valeur']; ?></span>
|
||||
<span class="stat-label"><?php echo htmlspecialchars($stat['label']); ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty(listeIconesCompetences())): ?>
|
||||
<!-- Bandeau défilant des outils, dérivé de la page Compétences -->
|
||||
<section class="logo-banner-section">
|
||||
<div class="logo-banner-header">
|
||||
<h2>Outils que je sais manipuler</h2>
|
||||
</div>
|
||||
<?php afficherLogos('tool'); ?>
|
||||
<?php afficherOutilsCompetences(); ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
+12
-6
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
@@ -9,6 +9,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
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')
|
||||
$category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool';
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$icon = trim($_POST['icon'] ?? '');
|
||||
$logo_url = trim($_POST['logourl'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
$position = (int) ($_POST['position'] ?? 0);
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vide
|
||||
if (!empty($name) && !empty($logo_url)) {
|
||||
// On vérifie que les champs obligatoires ne sont pas vides : il faut un nom, et
|
||||
// au moins l'un des deux visuels (icône Devicon ou image)
|
||||
if (!empty($name) && (!empty($icon) || !empty($logo_url))) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer le nouveau logo
|
||||
$stmt = $dbh->prepare('INSERT INTO logo_banners (category, name, 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
|
||||
$stmt->execute([
|
||||
':category' => $category,
|
||||
':name' => $name,
|
||||
':logo_url' => $logo_url,
|
||||
':icon' => $icon !== '' ? $icon : null,
|
||||
':logo_url' => $logo_url !== '' ? $logo_url : null,
|
||||
':link_url' => $link_url,
|
||||
':position' => $position
|
||||
]);
|
||||
@@ -48,7 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
echo "Veuillez remplir tous les champs obligatoires.";
|
||||
echo "Veuillez remplir le nom et au moins une icône ou une image.";
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
|
||||
+18
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
@@ -42,7 +45,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer le nouveau projet
|
||||
$stmt = $dbh->prepare('INSERT INTO projects (title, 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
|
||||
$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(':markdown', $markdown, 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();
|
||||
|
||||
// Si un PDF a été envoyé, on l'enregistre à part dans project_pdfs
|
||||
if ($pdfData !== null) {
|
||||
$nouvelId = (int) $dbh->lastInsertId();
|
||||
$stmtPdf = $dbh->prepare('INSERT INTO project_pdfs (project_id, pdf_filename, pdf_data) VALUES (:project_id, :pdf_filename, :pdf_data)');
|
||||
$stmtPdf->bindValue(':project_id', $nouvelId, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout du projet : " . $title);
|
||||
|
||||
|
||||
+18
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
@@ -42,7 +45,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!empty($title) && !empty($description) && !empty($duration) && !empty($image_url)) {
|
||||
try {
|
||||
// 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
|
||||
$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(':markdown', $markdown, 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();
|
||||
|
||||
// Si un PDF a été envoyé, on l'enregistre à part dans school_pdfs
|
||||
if ($pdfData !== null) {
|
||||
$nouvelId = (int) $dbh->lastInsertId();
|
||||
$stmtPdf = $dbh->prepare('INSERT INTO school_pdfs (school_id, pdf_filename, pdf_data) VALUES (:school_id, :pdf_filename, :pdf_data)');
|
||||
$stmtPdf->bindValue(':school_id', $nouvelId, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout de l'école : " . $title);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
@@ -9,6 +9,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// Ajout de connexion à la BDD
|
||||
require_once("../functions/databaseConnection.php");
|
||||
// Ajout des logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||
exit();
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Si le formulaire a été soumis
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
// Récupération des données du formulaire
|
||||
|
||||
+18
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
@@ -43,7 +46,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer la nouvelle experience
|
||||
$stmt = $dbh->prepare('INSERT INTO experiences (title, 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
|
||||
$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(':markdown', $markdown, 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();
|
||||
|
||||
// Si un PDF a été envoyé, on l'enregistre à part dans experience_pdfs
|
||||
if ($pdfData !== null) {
|
||||
$nouvelId = (int) $dbh->lastInsertId();
|
||||
$stmtPdf = $dbh->prepare('INSERT INTO experience_pdfs (experience_id, pdf_filename, pdf_data) VALUES (:experience_id, :pdf_filename, :pdf_data)');
|
||||
$stmtPdf->bindValue(':experience_id', $nouvelId, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout de l'expérience : " . $title);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Verification de l'utilisateur voir si ces ids sont bon ou pas
|
||||
|
||||
// On démarre une session au cas où le script soit exécuté directement par URL
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
@@ -38,6 +38,11 @@ if (!$user || !password_verify($password, $user['password'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Régénère l'ID de session à la connexion : empêche la fixation de session
|
||||
// (un attaquant qui aurait imposé un ID de session à la victime avant sa
|
||||
// connexion ne peut plus récupérer une session authentifiée avec cet ID)
|
||||
session_regenerate_id(true);
|
||||
|
||||
// Si la connexion est ok, on entregistre l'utilisateur dans $_SESSION
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
// Lorsque l'utilisateur est renvoyé à cette page/fonction, il est déconnecté
|
||||
|
||||
// Démarrage de session
|
||||
session_start();
|
||||
// Démarrage de session (cookie durci)
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
|
||||
// Ajout des logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
@@ -21,6 +21,7 @@ session_destroy();
|
||||
// Si le captcha avait déjà été validé, on recrée une session propre
|
||||
// pour conserver uniquement cette information
|
||||
if ($captchaAlreadyValid) {
|
||||
configureSecureSessionCookies();
|
||||
session_start();
|
||||
$_SESSION['captcha_valid'] = true;
|
||||
}
|
||||
|
||||
+20
-10
@@ -3,7 +3,7 @@
|
||||
// Le fichier téléchargé peut ensuite être ré-importé via importData.php
|
||||
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -15,22 +15,32 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Liste blanche des tables et colonnes exportées (empêche toute injection via noms de colonnes)
|
||||
// Depuis la migration Juillet 2026 (6), les PDF vivent dans des tables associées
|
||||
// (project_pdfs, experience_pdfs, school_pdfs) plutôt que dans projects/experiences/schools :
|
||||
// elles sont placées juste après leur table parente (l'ordre compte pour l'import, à cause
|
||||
// des clés étrangères ON DELETE CASCADE)
|
||||
$tables = [
|
||||
'settings' => ['name', 'value'],
|
||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||
'settings' => ['name', 'value'],
|
||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
|
||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'experience_pdfs' => ['experience_id', 'pdf_filename', 'pdf_data'],
|
||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'school_pdfs' => ['school_id', 'pdf_filename', 'pdf_data'],
|
||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||
];
|
||||
|
||||
// Colonnes binaires : le JSON ne peut pas contenir de binaire brut, on les encode en base64
|
||||
// (+33% de taille mais nécessaire pour que le PDF survive à un export/import complet)
|
||||
$colonnesBinaires = [
|
||||
'projects' => ['pdf_data'],
|
||||
'experiences' => ['pdf_data'],
|
||||
'schools' => ['pdf_data'],
|
||||
'project_pdfs' => ['pdf_data'],
|
||||
'experience_pdfs' => ['pdf_data'],
|
||||
'school_pdfs' => ['pdf_data'],
|
||||
];
|
||||
|
||||
// Métadonnées de l'export (pratique pour s'y retrouver dans les sauvegardes)
|
||||
|
||||
+20
-11
@@ -3,7 +3,7 @@
|
||||
// ATTENTION : les données actuelles des tables présentes dans le fichier sont ÉCRASÉES
|
||||
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -15,6 +15,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// La requête doit être un POST avec un fichier sinon on refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['jsonfile']) || $_FILES['jsonfile']['error'] !== UPLOAD_ERR_OK) {
|
||||
header('Location: ../pages/admin.php');
|
||||
@@ -29,21 +32,27 @@ if (!is_array($donnees)) {
|
||||
die("Erreur : le fichier n'est pas un JSON valide.");
|
||||
}
|
||||
|
||||
// Même liste blanche que l'export : seules ces tables / colonnes peuvent être importées
|
||||
// Même liste blanche que l'export : seules ces tables / colonnes peuvent être importées.
|
||||
// L'ORDRE COMPTE : chaque table de PDF associée (project_pdfs...) doit être importée
|
||||
// juste après sa table parente (projects...), car sa clé étrangère exige que la ligne
|
||||
// parente existe déjà (voir migration Juillet 2026 (6))
|
||||
$tables = [
|
||||
'settings' => ['name', 'value'],
|
||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||
'settings' => ['name', 'value'],
|
||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
|
||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'experience_pdfs' => ['experience_id', 'pdf_filename', 'pdf_data'],
|
||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||
'school_pdfs' => ['school_id', 'pdf_filename', 'pdf_data'],
|
||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||
];
|
||||
|
||||
// Colonnes binaires : encodées en base64 par exportData.php, à redécoder ici
|
||||
$colonnesBinaires = [
|
||||
'projects' => ['pdf_data'],
|
||||
'experiences' => ['pdf_data'],
|
||||
'schools' => ['pdf_data'],
|
||||
'project_pdfs' => ['pdf_data'],
|
||||
'experience_pdfs' => ['pdf_data'],
|
||||
'school_pdfs' => ['pdf_data'],
|
||||
];
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// Ajout de connexion à la BDD
|
||||
require_once("../functions/databaseConnection.php");
|
||||
// Ajout des logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||
exit();
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Récupération de l'ID passé en paramètre GET
|
||||
$id = $_GET["id"];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: ../pages/admin.php');
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<?php
|
||||
|
||||
if (!isset($_SESSION['captcha_valid']) || $_SESSION['captcha_valid'] !== true) {
|
||||
require_once(__DIR__ . '/../functions/listSettings.php');
|
||||
|
||||
// Le captcha peut être désactivé depuis le panel Admin (section Sécurité)
|
||||
$captchaEnabled = recupererSetting('captcha_enabled') ?? '1';
|
||||
|
||||
if ($captchaEnabled === '1' && (!isset($_SESSION['captcha_valid']) || $_SESSION['captcha_valid'] !== true)) {
|
||||
header('Location: ../captcha.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
require_once('../../idDiscord.php');
|
||||
|
||||
// Ajout des logs Discord
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Envoi de formulaire de contact via WebHook discord
|
||||
|
||||
+10
-5
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -12,10 +12,13 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
empty($_POST['name']) ||
|
||||
empty($_POST['logourl'])
|
||||
(empty($_POST['icon']) && empty($_POST['logourl']))
|
||||
) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -25,19 +28,21 @@ if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
$id = (int) $_POST['id'];
|
||||
$category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool';
|
||||
$name = trim($_POST['name']);
|
||||
$logourl = trim($_POST['logourl']);
|
||||
$icon = trim($_POST['icon'] ?? '');
|
||||
$logourl = trim($_POST['logourl'] ?? '');
|
||||
$linkurl = trim($_POST['linkurl'] ?? '');
|
||||
$position = (int) ($_POST['position'] ?? 0);
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('
|
||||
UPDATE logo_banners
|
||||
SET category = :category, name = :name, 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
|
||||
');
|
||||
$stmt->bindValue(':category', $category, 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(':position', $position, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
|
||||
+29
-13
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
@@ -61,15 +64,13 @@ if (is_array($pdfResultat)) {
|
||||
}
|
||||
|
||||
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
|
||||
SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url';
|
||||
if ($updatePdf) {
|
||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
||||
}
|
||||
$sql .= ' WHERE id = :id';
|
||||
|
||||
$stmt = $dbh->prepare($sql);
|
||||
SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
@@ -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(':markdown', $markdown, 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->execute(); // Sinon on renvoie l'erreur
|
||||
|
||||
if ($updatePdf) {
|
||||
if ($pdfData !== null) {
|
||||
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = project_id)
|
||||
$stmtPdf = $dbh->prepare('
|
||||
INSERT INTO project_pdfs (project_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||
');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
} else {
|
||||
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||
$stmtPdf = $dbh->prepare('DELETE FROM project_pdfs WHERE project_id = :id');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification du projet :" . $e->getMessage());
|
||||
}
|
||||
|
||||
+29
-13
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
@@ -62,15 +65,13 @@ if (is_array($pdfResultat)) {
|
||||
}
|
||||
|
||||
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
|
||||
SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url';
|
||||
if ($updatePdf) {
|
||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
||||
}
|
||||
$sql .= ' WHERE id = :id';
|
||||
|
||||
$stmt = $dbh->prepare($sql);
|
||||
SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
@@ -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(':markdown', $markdown, 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->execute(); // Sinon on renvoie l'erreur
|
||||
|
||||
if ($updatePdf) {
|
||||
if ($pdfData !== null) {
|
||||
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = school_id)
|
||||
$stmtPdf = $dbh->prepare('
|
||||
INSERT INTO school_pdfs (school_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||
');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
} else {
|
||||
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||
$stmtPdf = $dbh->prepare('DELETE FROM school_pdfs WHERE school_id = :id');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de l'école :" . $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
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
|
||||
// La checkbox n'est envoyée que si elle est cochée
|
||||
$reglages = [
|
||||
'site_notice_title' => trim($_POST['notice_title'] ?? ''),
|
||||
'site_notice_text' => trim($_POST['notice_text'] ?? ''),
|
||||
'site_notice_enabled' => isset($_POST['notice_enabled']) ? '1' : '0'
|
||||
];
|
||||
// settings_group : évite qu'un formulaire écrase les réglages d'un autre (ex : la case
|
||||
// "notice_enabled" non cochée ne doit pas désactiver le captcha, et inversement)
|
||||
$groupeReglages = $_POST['settings_group'] ?? 'notice';
|
||||
|
||||
if ($groupeReglages === 'captcha') {
|
||||
$reglages = [
|
||||
'captcha_enabled' => isset($_POST['captcha_enabled']) ? '1' : '0'
|
||||
];
|
||||
} elseif ($groupeReglages === 'contact') {
|
||||
$reglages = [
|
||||
'contact_email' => trim($_POST['contact_email'] ?? ''),
|
||||
'contact_email_enabled' => isset($_POST['contact_email_enabled']) ? '1' : '0',
|
||||
'contact_address_line1' => trim($_POST['contact_address_line1'] ?? ''),
|
||||
'contact_address_line2' => trim($_POST['contact_address_line2'] ?? ''),
|
||||
'contact_address_enabled' => isset($_POST['contact_address_enabled']) ? '1' : '0'
|
||||
];
|
||||
} else {
|
||||
$reglages = [
|
||||
'site_notice_title' => trim($_POST['notice_title'] ?? ''),
|
||||
'site_notice_text' => trim($_POST['notice_text'] ?? ''),
|
||||
'site_notice_enabled' => isset($_POST['notice_enabled']) ? '1' : '0'
|
||||
];
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée pour chaque réglage
|
||||
// ON DUPLICATE KEY UPDATE : crée le réglage s'il n'existe pas encore, le met à jour sinon
|
||||
@@ -41,7 +62,12 @@ try { // On essaie d'executer une requête qui est préparée pour chaque régla
|
||||
}
|
||||
|
||||
// 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
|
||||
header('Location: ../pages/admin.php');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
empty($_POST['title']) ||
|
||||
|
||||
+29
-13
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// On démarre la session
|
||||
session_start();
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||
requireCsrfToken();
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
@@ -65,15 +68,13 @@ if (is_array($pdfResultat)) {
|
||||
}
|
||||
|
||||
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
|
||||
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) {
|
||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
||||
}
|
||||
$sql .= ' WHERE id = :id';
|
||||
|
||||
$stmt = $dbh->prepare($sql);
|
||||
SET title = :title, status = :status, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
||||
@@ -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(':markdown', $markdown, 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->execute(); // Sinon on renvoie l'erreur
|
||||
|
||||
if ($updatePdf) {
|
||||
if ($pdfData !== null) {
|
||||
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = experience_id)
|
||||
$stmtPdf = $dbh->prepare('
|
||||
INSERT INTO experience_pdfs (experience_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||
');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||
$stmtPdf->execute();
|
||||
} else {
|
||||
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||
$stmtPdf = $dbh->prepare('DELETE FROM experience_pdfs WHERE experience_id = :id');
|
||||
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPdf->execute();
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de l experience :" . $e->getMessage());
|
||||
}
|
||||
|
||||
+6
-4
@@ -6,10 +6,12 @@
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Liste blanche des types autorisés : empêche toute injection de nom de table
|
||||
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6),
|
||||
// pour ne plus jamais charger son contenu binaire lors des listes de projets/xp/écoles)
|
||||
$tables = [
|
||||
'projet' => 'projects',
|
||||
'experience' => 'experiences',
|
||||
'ecole' => 'schools',
|
||||
'projet' => ['pdf' => 'project_pdfs', 'colonne' => 'project_id'],
|
||||
'experience' => ['pdf' => 'experience_pdfs', 'colonne' => 'experience_id'],
|
||||
'ecole' => ['pdf' => 'school_pdfs', 'colonne' => 'school_id'],
|
||||
];
|
||||
|
||||
$type = $_GET['type'] ?? '';
|
||||
@@ -21,7 +23,7 @@ if (!isset($tables[$type]) || !is_numeric($id)) {
|
||||
}
|
||||
|
||||
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->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
+96
-8
@@ -517,6 +517,44 @@ img {
|
||||
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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1349,8 +1387,10 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
/* Pas de fond/bordure : les logos flottent directement sur le fond de la page,
|
||||
comme un bandeau "trusted by" classique */
|
||||
/* Carte derrière chaque logo, avec un fond SOMBRE FIXE (pas var(--surface), qui devient
|
||||
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 {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -1359,17 +1399,21 @@ img {
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
padding: 18px 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
background: #14141c;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--radius);
|
||||
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 {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--accent-border);
|
||||
background: #1c1c26;
|
||||
}
|
||||
|
||||
.logo-tile img {
|
||||
.logo-tile img,
|
||||
.logo-tile i {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
@@ -1377,17 +1421,36 @@ a.logo-tile:hover {
|
||||
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);
|
||||
}
|
||||
|
||||
/* Sur mobile le survol n'existe pas : on affiche les logos en couleur directement */
|
||||
@media (hover: none) {
|
||||
.logo-tile img {
|
||||
.logo-tile img,
|
||||
.logo-tile i {
|
||||
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) {
|
||||
.logo-tile {
|
||||
width: 130px;
|
||||
@@ -1446,6 +1509,11 @@ a.logo-tile:hover {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-admin-preview i {
|
||||
font-size: 40px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.logo-admin-info {
|
||||
padding: 14px 16px 4px;
|
||||
}
|
||||
@@ -1564,6 +1632,14 @@ a.logo-tile:hover {
|
||||
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 */
|
||||
.skill-category .admin-actions {
|
||||
margin-top: 18px;
|
||||
@@ -1955,6 +2031,18 @@ a.card-link::after {
|
||||
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 */
|
||||
.admin-block {
|
||||
max-width: 1200px;
|
||||
|
||||
Reference in new issue
Block a user