Compare commits
10
Commits
4862d61ccd
...
a28e0891d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a28e0891d4 | ||
|
|
e5bd31fc65 | ||
|
|
a4ccd7e2a9 | ||
|
|
e26dbd388f | ||
|
|
a1c99f1de8 | ||
|
|
c04a9b5ab8 | ||
|
|
c28fae7061 | ||
|
|
91060537c8 | ||
|
|
5b76997895 | ||
|
|
4c35c727d5 |
No files matched your search
+13
@@ -0,0 +1,13 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Éditeurs et outils locaux
|
||||
.vscode/
|
||||
.idea/
|
||||
.claude/
|
||||
*.iml
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
@@ -11,8 +11,8 @@ La réutilisation du projet est autorisée, sous réserve que les fichiers `idCo
|
||||
## Informations techniques
|
||||
|
||||
- La base de données peut être restaurée rapidement à l'aide du fichier `scriptDatabase.sql`.
|
||||
- Le CSS a été généré avec l'aide d'une intelligence artificielle.
|
||||
- Le code PHP a été réalisé en grande partie par l'auteur.
|
||||
- Le site est développé en PHP, sans framework, avec MySQL/MariaDB pour le stockage.
|
||||
- Le déploiement se fait via Docker (voir le dossier `docker/`).
|
||||
|
||||
## Signalement de vulnérabilité
|
||||
|
||||
|
||||
+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>';
|
||||
}
|
||||
|
||||
|
||||
+117
-2
@@ -32,6 +32,42 @@ 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 du bandeau en thème sombre. On leur laisse la couleur par défaut, qui suit le
|
||||
// thème actif (voir .logo-tile i:not(.colored) dans styles/primary_css.css).
|
||||
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 +89,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 +100,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 +134,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.");
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
<?php
|
||||
// Ce projet est entièrement développé par Esteban MOUSSU
|
||||
// 2026 - Projet Hébergé par l'entreprise TerraTech Hebergement en collaboration avec l'association Eldoria Network
|
||||
// Le CSS est généré par intelligence artificielle ou récupéré d'autre projet.
|
||||
// Note 1 : Pour lancer un serveur local PHP Depuis VsCode : 'php -v' pour vérifier que php est bien installé et 'php -S localhost:8000' pour lancer le serveur
|
||||
// Note 2 : Je m'aide désormais de l'autocompletion de vsCode pour accélerer le développement avec Copilot (GitHub) ou Devin mais le reste du code est entièrement développé par moi même mais copilot
|
||||
// reste utile pour la génération de commentaires, de code répétitif ou de code basique (exemple : les formulaires de contact, les requêtes SQL basiques etc...)
|
||||
// Note : Pour lancer un serveur local PHP depuis VsCode : 'php -v' pour vérifier que php est bien installé et 'php -S localhost:8000' pour lancer le serveur
|
||||
|
||||
|
||||
// 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>
|
||||
|
||||
+8
-13
@@ -27,38 +27,33 @@ require_once('./../elements/header.php');
|
||||
<p>L’ensemble des éléments présents sur le site, notamment les textes, visuels, illustrations, logos, éléments graphiques, structure, code source et contenus originaux, est protégé par le droit de la propriété intellectuelle, sauf mention contraire.</p>
|
||||
<p>Toute reproduction, représentation, diffusion, adaptation, modification ou exploitation, totale ou partielle, sans autorisation préalable écrite, est interdite, sauf dans les cas autorisés par la loi.</p>
|
||||
|
||||
<h3> 4. Utilisation de l’intelligence artificielle</h3>
|
||||
<p>Le site et certains de ses éléments ont pu être conçus avec l'assistance d'outils d’intelligence artificielle. Cela peut notamment concerner la génération ou l’adaptation de feuilles de style CSS, l’aide à la rédaction, l’autocomplétion de code, la génération de commentaires, de structures répétitives, de formulaires simples ou de requêtes basiques.</p>
|
||||
<p>L’éditeur précise cependant que le développement principal du projet, notamment le code PHP, HTML et JavaScript, l’architecture générale, l’intégration, les choix techniques et la personnalisation globale du site, ont été réalisés majoritairement par un humain, à savoir Esteban MOUSSU.</p>
|
||||
<p>L'utilisation d'outils tels que GitHub Copilot, Devin ou d'autres assistants similaires constitue une aide au développement et non une substitution complète au travail humain.</p>
|
||||
|
||||
<h3> 5. Hébergement et édition</h3>
|
||||
<h3> 4. Hébergement et édition</h3>
|
||||
<p>Ce projet est entièrement développé par Esteban MOUSSU.</p>
|
||||
<p>Le site est hébergé en 2026 par l’entreprise TerraTech Hebergement, en collaboration avec l’association Eldoria Network.</p>
|
||||
<p>Les informations diffusées sur le site le sont sous la responsabilité de son éditeur.</p>
|
||||
|
||||
<h3> 6. Exactitude des informations</h3>
|
||||
<h3> 5. Exactitude des informations</h3>
|
||||
<p>L’éditeur s’efforce de fournir des informations aussi exactes et à jour que possible. Toutefois, aucune garantie n’est donnée quant à l’exactitude, l’exhaustivité ou l’actualité des contenus publiés.</p>
|
||||
<p>L’utilisateur reconnaît utiliser les informations disponibles sur le site sous sa seule responsabilité.</p>
|
||||
|
||||
<h3> 7. Liens externes</h3>
|
||||
<h3> 6. Liens externes</h3>
|
||||
<p>Le site peut contenir des liens vers des sites tiers. Ces liens sont fournis à titre informatif. L’éditeur ne saurait être tenu responsable du contenu, du fonctionnement ou des politiques de confidentialité de ces sites externes.</p>
|
||||
|
||||
<h3> 8. Responsabilité</h3>
|
||||
<h3> 7. Responsabilité</h3>
|
||||
<p>L’éditeur ne pourra être tenu responsable des dommages directs ou indirects résultant de l’utilisation du site, d’une interruption temporaire, d’un dysfonctionnement technique ou d’une impossibilité d’accès.</p>
|
||||
<p>L’utilisateur demeure seul responsable de l’usage qu’il fait du site et des informations qu’il y consulte.</p>
|
||||
|
||||
<h3> 9. Données personnelles</h3>
|
||||
<h3> 8. Données personnelles</h3>
|
||||
<p>Le site peut proposer un ou plusieurs formulaires de contact. Les données éventuellement transmises par l’utilisateur sont uniquement destinées au traitement de sa demande, sauf mention contraire clairement indiquée.</p>
|
||||
<p>Conformément à la réglementation applicable, l’utilisateur peut demander l’accès, la rectification ou la suppression de ses données en contactant l’éditeur du site.</p>
|
||||
|
||||
<h3> 10. Sécurité</h3>
|
||||
<h3> 9. Sécurité</h3>
|
||||
<p>L’utilisateur s’interdit de perturber le bon fonctionnement du site, d’introduire des programmes malveillants, de tenter un accès non autorisé ou de porter atteinte à l’intégrité technique du service.</p>
|
||||
|
||||
<h3> 11. Modification des CGU</h3>
|
||||
<h3> 10. Modification des CGU</h3>
|
||||
<p>L’éditeur se réserve le droit de modifier à tout moment les présentes CGU. Les modifications prennent effet dès leur publication sur cette page.</p>
|
||||
|
||||
<h3> 12. Droit applicable</h3>
|
||||
<h3> 11. Droit applicable</h3>
|
||||
<p>Les présentes CGU sont soumises au droit français. En cas de litige, et à défaut de résolution amiable, les juridictions compétentes seront celles déterminées par les règles de droit commun.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+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);
|
||||
|
||||
+133
-14
@@ -1,5 +1,3 @@
|
||||
/* ---- LE CSS EST SOIT GÉNÉRÉ PAR INTEL ARTI SOIT RÉUTILISÉ D'UN AUTRE PROJET ---- */
|
||||
|
||||
/* ---- Variables globales du thème (sombre par défaut) ---- */
|
||||
:root {
|
||||
--bg: #0a0a10;
|
||||
@@ -517,6 +515,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 +1385,11 @@ 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 : suit le thème actif comme le reste des cartes du site.
|
||||
C'est possible parce que les logos posés dessus ne sont plus affichés tels quels mais
|
||||
aplatis en silhouette monochrome adaptée au thème (voir la règle suivante) : sans ça,
|
||||
un logo blanc sur fond transparent (3S Sécurité, TerraTech) disparaîtrait sur une
|
||||
tuile claire, et un logo noir (Magyar, Gamm Vert) sur une tuile sombre */
|
||||
.logo-tile {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -1359,33 +1398,88 @@ img {
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
padding: 18px 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
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: var(--surface-2);
|
||||
}
|
||||
|
||||
.logo-tile img {
|
||||
/* Bandeau "Ils m'ont fait confiance" : les logos fournis par les entreprises ont des
|
||||
teintes très variables (certains blancs, d'autres noirs, tous sur fond transparent).
|
||||
brightness(0) les aplatit en silhouette noire, invert(1) la repasse en blanc pour le
|
||||
thème sombre : lisible dans les deux thèmes, et rendu uniforme façon mur de partenaires.
|
||||
Le bandeau des outils, lui, garde les vraies couleurs (voir .logo-marquee-tools) */
|
||||
.logo-tile img,
|
||||
.logo-tile i {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
filter: grayscale(1) opacity(0.6);
|
||||
filter: brightness(0) invert(1) opacity(0.55);
|
||||
transition: filter 0.25s ease;
|
||||
}
|
||||
|
||||
.logo-tile:hover img {
|
||||
filter: grayscale(0) opacity(1);
|
||||
[data-theme="light"] .logo-tile img,
|
||||
[data-theme="light"] .logo-tile i {
|
||||
filter: brightness(0) opacity(0.45);
|
||||
}
|
||||
|
||||
/* Sur mobile le survol n'existe pas : on affiche les logos en couleur directement */
|
||||
/* Taille de la glyphe pour les icônes Devicon (les images gardent leur taille naturelle) */
|
||||
/* La couleur suit le thème, sinon l'icône serait invisible sur la tuile en thème clair.
|
||||
:not(.colored) laisse intactes les icônes Devicon affichées aux couleurs de la marque
|
||||
dans le bandeau des outils (voir afficherVisuelItem dans functions/listSkills.php) */
|
||||
.logo-tile i {
|
||||
font-size: 44px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.logo-tile i:not(.colored) {
|
||||
color: #e8e8f2;
|
||||
}
|
||||
|
||||
[data-theme="light"] .logo-tile i:not(.colored) {
|
||||
color: #191924;
|
||||
}
|
||||
|
||||
.logo-tile:hover img,
|
||||
.logo-tile:hover i {
|
||||
filter: brightness(0) invert(1) opacity(1);
|
||||
}
|
||||
|
||||
[data-theme="light"] .logo-tile:hover img,
|
||||
[data-theme="light"] .logo-tile:hover i {
|
||||
filter: brightness(0) opacity(1);
|
||||
}
|
||||
|
||||
/* Sur mobile le survol n'existe pas : on affiche les logos plus contrastés directement */
|
||||
@media (hover: none) {
|
||||
.logo-tile img {
|
||||
filter: grayscale(0) opacity(0.85);
|
||||
.logo-tile img,
|
||||
.logo-tile i {
|
||||
filter: brightness(0) invert(1) opacity(0.8);
|
||||
}
|
||||
|
||||
[data-theme="light"] .logo-tile img,
|
||||
[data-theme="light"] .logo-tile i {
|
||||
filter: brightness(0) opacity(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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,
|
||||
.logo-marquee-tools .logo-tile:hover img,
|
||||
.logo-marquee-tools .logo-tile:hover i,
|
||||
[data-theme="light"] .logo-marquee-tools .logo-tile img,
|
||||
[data-theme="light"] .logo-marquee-tools .logo-tile i {
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
@@ -1446,6 +1540,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 +1663,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 +2062,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