Fix lag
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# Config Claude Code (local, pas utile au dépôt)
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Éditeurs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once(__DIR__ . '/functions/security.php');
|
||||||
|
|
||||||
// Ajout des logs Discord (chemin via __DIR__ car captcha.php est à la racine du site)
|
// Ajout des logs Discord (chemin via __DIR__ car captcha.php est à la racine du site)
|
||||||
require_once(__DIR__ . '/functions/sendLogs.php');
|
require_once(__DIR__ . '/functions/sendLogs.php');
|
||||||
|
|||||||
@@ -179,4 +179,65 @@ ADD COLUMN pdf_filename VARCHAR(255) NULL;
|
|||||||
-- (validé côté PHP), pour les outils absents de Devicon (Proxmox, GLPI, FOG...)
|
-- (validé côté PHP), pour les outils absents de Devicon (Proxmox, GLPI, FOG...)
|
||||||
ALTER TABLE logo_banners
|
ALTER TABLE logo_banners
|
||||||
ADD COLUMN icon VARCHAR(100) NULL AFTER name,
|
ADD COLUMN icon VARCHAR(100) NULL AFTER name,
|
||||||
MODIFY COLUMN logo_url VARCHAR(255) NULL;
|
MODIFY COLUMN logo_url VARCHAR(255) NULL;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Juillet 2026 (6) : sortie des PDF des tables projects/experiences/schools
|
||||||
|
-- vers des tables associées dédiées, pour que les pages qui LISTENT ces
|
||||||
|
-- éléments (admin.php, xp.php, projects.php...) n'aient plus à charger les
|
||||||
|
-- PDF (jusqu'à 15 Mo chacun) en mémoire à chaque affichage. C'était la
|
||||||
|
-- cause du ralentissement d'admin.php et de xp.php : ces pages faisaient
|
||||||
|
-- un SELECT * qui remontait aussi tout le contenu binaire des PDF alors
|
||||||
|
-- que seul le nom de fichier est utilisé dans les listes.
|
||||||
|
--
|
||||||
|
-- ÉTAPE 1/2 (non destructive) : crée les nouvelles tables et copie les PDF
|
||||||
|
-- existants dedans. Les colonnes pdf_data / pdf_filename des tables
|
||||||
|
-- d'origine ne sont PAS touchées à cette étape : aucune perte de donnée
|
||||||
|
-- possible, elles restent en place comme copie de sécurité.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS project_pdfs (
|
||||||
|
project_id INT UNSIGNED NOT NULL,
|
||||||
|
pdf_filename VARCHAR(255) NOT NULL,
|
||||||
|
pdf_data LONGBLOB NOT NULL,
|
||||||
|
PRIMARY KEY (project_id),
|
||||||
|
CONSTRAINT fk_project_pdfs_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS experience_pdfs (
|
||||||
|
experience_id INT UNSIGNED NOT NULL,
|
||||||
|
pdf_filename VARCHAR(255) NOT NULL,
|
||||||
|
pdf_data LONGBLOB NOT NULL,
|
||||||
|
PRIMARY KEY (experience_id),
|
||||||
|
CONSTRAINT fk_experience_pdfs_experience FOREIGN KEY (experience_id) REFERENCES experiences(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS school_pdfs (
|
||||||
|
school_id INT UNSIGNED NOT NULL,
|
||||||
|
pdf_filename VARCHAR(255) NOT NULL,
|
||||||
|
pdf_data LONGBLOB NOT NULL,
|
||||||
|
PRIMARY KEY (school_id),
|
||||||
|
CONSTRAINT fk_school_pdfs_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Copie des PDF déjà en base vers les nouvelles tables (ne copie que les lignes qui ont
|
||||||
|
-- effectivement un PDF ; ré-exécutable sans doublon grâce à la clé primaire = id du parent)
|
||||||
|
INSERT IGNORE INTO project_pdfs (project_id, pdf_filename, pdf_data)
|
||||||
|
SELECT id, pdf_filename, pdf_data FROM projects WHERE pdf_data IS NOT NULL;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO experience_pdfs (experience_id, pdf_filename, pdf_data)
|
||||||
|
SELECT id, pdf_filename, pdf_data FROM experiences WHERE pdf_data IS NOT NULL;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO school_pdfs (school_id, pdf_filename, pdf_data)
|
||||||
|
SELECT id, pdf_filename, pdf_data FROM schools WHERE pdf_data IS NOT NULL;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- ÉTAPE 2/2 (destructive, À NE PAS EXÉCUTER MAINTENANT) : à lancer plus
|
||||||
|
-- tard, une fois que le site a été vérifié (listes, modification, suppression,
|
||||||
|
-- lecteur PDF) et qu'on est sûr à 100% que tout fonctionne avec les
|
||||||
|
-- nouvelles tables. Supprime les anciennes colonnes devenues inutiles.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ALTER TABLE projects DROP COLUMN pdf_data, DROP COLUMN pdf_filename;
|
||||||
|
-- ALTER TABLE experiences DROP COLUMN pdf_data, DROP COLUMN pdf_filename;
|
||||||
|
-- ALTER TABLE schools DROP COLUMN pdf_data, DROP COLUMN pdf_filename;
|
||||||
+2
-1
@@ -52,7 +52,8 @@ if (isset($_GET['success']) && $_GET['success'] === '1') {
|
|||||||
var cibles = document.querySelectorAll(
|
var cibles = document.querySelectorAll(
|
||||||
'.project-card, .quick-link-card, .skill-category, .contact-card, ' +
|
'.project-card, .quick-link-card, .skill-category, .contact-card, ' +
|
||||||
'.login-card, .project-form-card, .cgu-card, .logo-admin-item, ' +
|
'.login-card, .project-form-card, .cgu-card, .logo-admin-item, ' +
|
||||||
'.page-header, .quick-links-header, .skills-header, .projects-title, .logo-banner-header'
|
'.page-header, .quick-links-header, .skills-header, .projects-title, .logo-banner-header, ' +
|
||||||
|
'.stat-item'
|
||||||
);
|
);
|
||||||
|
|
||||||
var observer = new IntersectionObserver(function (entries) {
|
var observer = new IntersectionObserver(function (entries) {
|
||||||
|
|||||||
+26
-1
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// Ajout ici d'un démarrage de session : header étant dans toutes les pages
|
// Ajout ici d'un démarrage de session : header étant dans toutes les pages
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
|
||||||
// Ajout ici la vérification du captcha
|
// Ajout ici la vérification du captcha
|
||||||
require_once('../scripts/requireCaptcha.php');
|
require_once('../scripts/requireCaptcha.php');
|
||||||
@@ -20,6 +20,15 @@ sendPageLog();
|
|||||||
// Titre de la page : chaque page peut définir $pageTitle avant d'inclure le header
|
// Titre de la page : chaque page peut définir $pageTitle avant d'inclure le header
|
||||||
$pageTitle = isset($pageTitle) ? $pageTitle . ' — Esteban MOUSSU' : 'Esteban MOUSSU — Portfolio';
|
$pageTitle = isset($pageTitle) ? $pageTitle . ' — Esteban MOUSSU' : 'Esteban MOUSSU — Portfolio';
|
||||||
|
|
||||||
|
// Description : chaque page peut définir $pageDescription avant d'inclure le header
|
||||||
|
// (sinon description générique, utilisée pour le SEO et les aperçus de partage)
|
||||||
|
$pageDescription = isset($pageDescription)
|
||||||
|
? $pageDescription
|
||||||
|
: 'Portfolio d\'Esteban MOUSSU, étudiant en informatique passionné par les systèmes, les réseaux et le développement web. Projets, expériences et compétences.';
|
||||||
|
|
||||||
|
// URL absolue de la page courante (pour og:url et le lien canonique)
|
||||||
|
$pageUrl = 'https://estebanadmin.fr' . strtok($_SERVER['REQUEST_URI'], '?');
|
||||||
|
|
||||||
// Page courante pour souligner le lien actif dans la navigation
|
// Page courante pour souligner le lien actif dans la navigation
|
||||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||||
?>
|
?>
|
||||||
@@ -31,6 +40,22 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?php echo htmlspecialchars($pageTitle); ?></title>
|
<title><?php echo htmlspecialchars($pageTitle); ?></title>
|
||||||
|
|
||||||
|
<!-- SEO : description + lien canonique -->
|
||||||
|
<meta name="description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||||
|
<link rel="canonical" href="<?php echo htmlspecialchars($pageUrl); ?>">
|
||||||
|
|
||||||
|
<!-- Aperçu de partage (réseaux sociaux, Discord, etc.) -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="<?php echo htmlspecialchars($pageTitle); ?>">
|
||||||
|
<meta property="og:description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||||
|
<meta property="og:url" content="<?php echo htmlspecialchars($pageUrl); ?>">
|
||||||
|
<meta property="og:image" content="https://estebanadmin.fr/memoji.png">
|
||||||
|
<meta property="og:locale" content="fr_FR">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="<?php echo htmlspecialchars($pageTitle); ?>">
|
||||||
|
<meta name="twitter:description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||||
|
<meta name="twitter:image" content="https://estebanadmin.fr/memoji.png">
|
||||||
|
|
||||||
<!-- Favicon du site -->
|
<!-- Favicon du site -->
|
||||||
<link rel="icon" type="image/png" href="../memoji.png">
|
<link rel="icon" type="image/png" href="../memoji.png">
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ function afficherLogosAdmin() {
|
|||||||
echo '</div>';
|
echo '</div>';
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_logo=' . $id . '#admin-logos">Modifier</a>';
|
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_logo=' . $id . '#admin-logos">Modifier</a>';
|
||||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeLogo.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce logo ?\');">Supprimer</a>';
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeLogo.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce logo ?\');">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,22 @@ function listeProjets() {
|
|||||||
|
|
||||||
// Essayer
|
// Essayer
|
||||||
try {
|
try {
|
||||||
// Requête SQL qui récupère titre, tags, description, image_url
|
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||||
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
|
// stocké à part dans project_pdfs (voir migration Juillet 2026 (6)), on ne récupère
|
||||||
// même si la migration SQL n'a pas encore été exécutée)
|
// ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
|
||||||
$stmt = $dbh->prepare('SELECT * FROM projects ORDER BY id DESC');
|
// (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste de projets
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT p.id, p.title, p.status, p.tags, p.description, p.image_url, p.markdown, p.link_url, p.created_at,
|
||||||
|
pp.pdf_filename
|
||||||
|
FROM projects p
|
||||||
|
LEFT JOIN project_pdfs pp ON pp.project_id = p.id
|
||||||
|
ORDER BY p.id DESC
|
||||||
|
');
|
||||||
// Execution de la requête SQL
|
// Execution de la requête SQL
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
// Retourne toutes les données collectés dans un ARRAY
|
// Retourne toutes les données collectés dans un ARRAY
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (PDOException $e) { // En cas d'erreur
|
} catch (PDOException $e) { // En cas d'erreur
|
||||||
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -29,7 +36,15 @@ function recupererProjetParId($id) {
|
|||||||
global $dbh;
|
global $dbh;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $dbh->prepare('SELECT * FROM projects WHERE id = :id');
|
// Même principe que listeProjets() : le PDF est joint pour son nom de fichier
|
||||||
|
// uniquement, jamais son contenu binaire
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT p.id, p.title, p.status, p.tags, p.description, p.image_url, p.markdown, p.link_url, p.created_at,
|
||||||
|
pp.pdf_filename
|
||||||
|
FROM projects p
|
||||||
|
LEFT JOIN project_pdfs pp ON pp.project_id = p.id
|
||||||
|
WHERE p.id = :id
|
||||||
|
');
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
@@ -164,7 +179,7 @@ function afficherProjetsAdmin() {
|
|||||||
// Boutons qui vont permettrent la modification / Suppresion
|
// Boutons qui vont permettrent la modification / Suppresion
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '#admin-projets">Modifier</a>';
|
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '#admin-projets">Modifier</a>';
|
||||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeProject.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce projet ?\');">Supprimer</a>';
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeProject.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce projet ?\');">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|||||||
@@ -10,9 +10,17 @@ function listeEcoles() {
|
|||||||
global $dbh;
|
global $dbh;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
|
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||||
// même si la migration SQL n'a pas encore été exécutée)
|
// stocké à part dans school_pdfs (voir migration Juillet 2026 (6)), on ne récupère
|
||||||
$stmt = $dbh->prepare('SELECT * FROM schools ORDER BY id DESC');
|
// ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
|
||||||
|
// (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste d'écoles
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT s.id, s.title, s.status, s.tags, s.description, s.duration, s.image_url, s.markdown, s.link_url, s.created_at,
|
||||||
|
sp.pdf_filename
|
||||||
|
FROM schools s
|
||||||
|
LEFT JOIN school_pdfs sp ON sp.school_id = s.id
|
||||||
|
ORDER BY s.id DESC
|
||||||
|
');
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
@@ -25,7 +33,15 @@ function recupererEcoleParId($id) {
|
|||||||
global $dbh;
|
global $dbh;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $dbh->prepare('SELECT * FROM schools WHERE id = :id');
|
// Même principe que listeEcoles() : le PDF est joint pour son nom de fichier
|
||||||
|
// uniquement, jamais son contenu binaire
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT s.id, s.title, s.status, s.tags, s.description, s.duration, s.image_url, s.markdown, s.link_url, s.created_at,
|
||||||
|
sp.pdf_filename
|
||||||
|
FROM schools s
|
||||||
|
LEFT JOIN school_pdfs sp ON sp.school_id = s.id
|
||||||
|
WHERE s.id = :id
|
||||||
|
');
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
@@ -98,7 +114,7 @@ function afficherEcoleCard($ecole, $admin = false) {
|
|||||||
if ($admin) {
|
if ($admin) {
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_school=' . $id . '#admin-ecoles">Modifier</a>';
|
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_school=' . $id . '#admin-ecoles">Modifier</a>';
|
||||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeSchool.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette école ?\');">Supprimer</a>';
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeSchool.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette école ?\');">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ function afficherSkillCard($skill, $admin = false) {
|
|||||||
if ($admin) {
|
if ($admin) {
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_skill=' . $id . '#admin-competences">Modifier</a>';
|
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_skill=' . $id . '#admin-competences">Modifier</a>';
|
||||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeSkill.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette catégorie de compétences ?\');">Supprimer</a>';
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeSkill.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette catégorie de compétences ?\');">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 '<p><strong>Créé le :</strong> ' . htmlspecialchars($user["created_at"]) . '</p>';
|
||||||
|
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a href="../scripts/removeUser.php?id=' . $user["id"] . '" class="btn-submit btn-delete" onclick="return confirm(\'Supprimer cet utilisateur ?\')">Supprimer</a>';
|
echo '<a href="../scripts/removeUser.php?id=' . $user["id"] . '&csrf_token=' . urlencode(csrfToken()) . '" class="btn-submit btn-delete" onclick="return confirm(\'Supprimer cet utilisateur ?\')">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|||||||
+23
-9
@@ -13,15 +13,22 @@ function listeXP() {
|
|||||||
|
|
||||||
// Essayer
|
// Essayer
|
||||||
try {
|
try {
|
||||||
// Requête SQL qui récupère titre, type, tags, description, durée, image_url
|
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||||
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
|
// stocké à part dans experience_pdfs (voir migration Juillet 2026 (6)), on ne
|
||||||
// même si la migration SQL n'a pas encore été exécutée)
|
// récupère ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu
|
||||||
$stmt = $dbh->prepare('SELECT * FROM experiences ORDER BY id DESC');
|
// binaire (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste d'expériences
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT e.id, e.title, e.status, e.xptype, e.tags, e.description, e.duration, e.image_url, e.markdown, e.link_url, e.created_at,
|
||||||
|
ep.pdf_filename
|
||||||
|
FROM experiences e
|
||||||
|
LEFT JOIN experience_pdfs ep ON ep.experience_id = e.id
|
||||||
|
ORDER BY e.id DESC
|
||||||
|
');
|
||||||
// Execution de la requête SQL
|
// Execution de la requête SQL
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
// Retourne toutes les données collectés dans un ARRAY
|
// Retourne toutes les données collectés dans un ARRAY
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (PDOException $e) { // En cas d'erreur
|
} catch (PDOException $e) { // En cas d'erreur
|
||||||
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -32,13 +39,20 @@ function recupererXPParId($id) {
|
|||||||
global $dbh;
|
global $dbh;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Préparation de la requête préparée
|
// Même principe que listeXP() : le PDF est joint pour son nom de fichier
|
||||||
$stmt = $dbh->prepare('SELECT * FROM experiences WHERE id = :id');
|
// uniquement, jamais son contenu binaire
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT e.id, e.title, e.status, e.xptype, e.tags, e.description, e.duration, e.image_url, e.markdown, e.link_url, e.created_at,
|
||||||
|
ep.pdf_filename
|
||||||
|
FROM experiences e
|
||||||
|
LEFT JOIN experience_pdfs ep ON ep.experience_id = e.id
|
||||||
|
WHERE e.id = :id
|
||||||
|
');
|
||||||
|
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
// En cas d'erreur => renvoyer un message avec l'erreur
|
// En cas d'erreur => renvoyer un message avec l'erreur
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur de connexion à la base de donnée portfolio :" . $e->getMessage());
|
die("Erreur de connexion à la base de donnée portfolio :" . $e->getMessage());
|
||||||
// Retourner false en cas d'eerreur
|
// Retourner false en cas d'eerreur
|
||||||
@@ -195,7 +209,7 @@ function afficherXPAdmin() {
|
|||||||
// Boutons qui vont permettrent la modification / Suppresion
|
// Boutons qui vont permettrent la modification / Suppresion
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '#admin-experiences">Modifier</a>';
|
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '#admin-experiences">Modifier</a>';
|
||||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeXP.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette expérience ?\');">Supprimer</a>';
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeXP.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette expérience ?\');">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
// Sécurité transverse : cookie de session durci, en-têtes anti-clickjacking et
|
||||||
|
// protection CSRF. A inclure à la place d'un simple session_start() sur toute
|
||||||
|
// page ou script qui utilise une session (remplace l'appel direct partout).
|
||||||
|
|
||||||
|
// Règle les paramètres du cookie de session AVANT de démarrer la session (ils
|
||||||
|
// n'ont aucun effet une fois la session déjà démarrée) :
|
||||||
|
// - httponly : empêche le JavaScript de lire le cookie (limite le vol de session via une XSS)
|
||||||
|
// - samesite=Lax : bloque une partie des requêtes intersites (CSRF)
|
||||||
|
// - secure : n'est activé que si la connexion est déjà en HTTPS (sinon le cookie ne partirait jamais en HTTP)
|
||||||
|
function configureSecureSessionCookies() {
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anti-clickjacking : interdit d'afficher le site dans une <iframe> d'un autre site
|
||||||
|
header('X-Frame-Options: SAMEORIGIN');
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
configureSecureSessionCookies();
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génère (si besoin) et retourne le jeton CSRF de la session en cours, à
|
||||||
|
// placer dans un champ caché de chaque formulaire admin ou dans les liens
|
||||||
|
// GET qui déclenchent une action (suppression, export...)
|
||||||
|
function csrfToken() {
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifie le jeton CSRF reçu (POST ou GET) ; arrête le script si invalide.
|
||||||
|
// A appeler juste après la vérification du rôle admin dans chaque script d'action.
|
||||||
|
function requireCsrfToken() {
|
||||||
|
$recu = $_POST['csrf_token'] ?? $_GET['csrf_token'] ?? '';
|
||||||
|
if (empty($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $recu)) {
|
||||||
|
http_response_code(403);
|
||||||
|
die("Requête refusée : jeton de sécurité invalide ou expiré. Recharge la page et réessaie.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// Le header n'y étant pas je rajoute ici un démarrage de session
|
// Le header n'y étant pas je rajoute ici un démarrage de session
|
||||||
session_start();
|
require_once(__DIR__ . '/functions/security.php');
|
||||||
|
|
||||||
// Redirection de l'utilisateur vers une page Home lorsqu'il arrive sur index.php
|
// Redirection de l'utilisateur vers une page Home lorsqu'il arrive sur index.php
|
||||||
header('location: ./pages/home.php');
|
header('location: ./pages/home.php');
|
||||||
|
|||||||
+9
-1
@@ -87,6 +87,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
<!-- enctype multipart/form-data : nécessaire pour l'upload du PDF -->
|
<!-- enctype multipart/form-data : nécessaire pour l'upload du PDF -->
|
||||||
<!-- en envoie les données à addProject.php -->
|
<!-- en envoie les données à addProject.php -->
|
||||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $projetAModifier ? '../scripts/updateProject.php' : '../scripts/addProject.php'; ?>">
|
<form method="POST" enctype="multipart/form-data" action="<?php echo $projetAModifier ? '../scripts/updateProject.php' : '../scripts/addProject.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
<?php if ($projetAModifier): ?>
|
<?php if ($projetAModifier): ?>
|
||||||
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
|
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
|
||||||
@@ -151,6 +152,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
<div class="project-form-card">
|
<div class="project-form-card">
|
||||||
<!-- en envoie les données à addXP.php -->
|
<!-- en envoie les données à addXP.php -->
|
||||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $xpAModifier ? '../scripts/updateXP.php' : '../scripts/addXP.php'; ?>">
|
<form method="POST" enctype="multipart/form-data" action="<?php echo $xpAModifier ? '../scripts/updateXP.php' : '../scripts/addXP.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
<?php if ($xpAModifier): ?>
|
<?php if ($xpAModifier): ?>
|
||||||
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
|
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
|
||||||
@@ -221,6 +223,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
<div class="project-form-card">
|
<div class="project-form-card">
|
||||||
<!-- on envoie les données à addSchool.php -->
|
<!-- on envoie les données à addSchool.php -->
|
||||||
<form method="POST" enctype="multipart/form-data" action="<?php echo $ecoleAModifier ? '../scripts/updateSchool.php' : '../scripts/addSchool.php'; ?>">
|
<form method="POST" enctype="multipart/form-data" action="<?php echo $ecoleAModifier ? '../scripts/updateSchool.php' : '../scripts/addSchool.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
<?php if ($ecoleAModifier): ?>
|
<?php if ($ecoleAModifier): ?>
|
||||||
<input type="hidden" name="id" value="<?php echo (int) $ecoleAModifier['id']; ?>">
|
<input type="hidden" name="id" value="<?php echo (int) $ecoleAModifier['id']; ?>">
|
||||||
@@ -288,6 +291,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
<div class="project-form-card">
|
<div class="project-form-card">
|
||||||
<!-- on envoie les données à addSkill.php -->
|
<!-- on envoie les données à addSkill.php -->
|
||||||
<form method="POST" action="<?php echo $skillAModifier ? '../scripts/updateSkill.php' : '../scripts/addSkill.php'; ?>">
|
<form method="POST" action="<?php echo $skillAModifier ? '../scripts/updateSkill.php' : '../scripts/addSkill.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
<?php if ($skillAModifier): ?>
|
<?php if ($skillAModifier): ?>
|
||||||
<input type="hidden" name="id" value="<?php echo (int) $skillAModifier['id']; ?>">
|
<input type="hidden" name="id" value="<?php echo (int) $skillAModifier['id']; ?>">
|
||||||
@@ -332,6 +336,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
<div class="project-form-card">
|
<div class="project-form-card">
|
||||||
<!-- on envoie les données à addLogo.php -->
|
<!-- on envoie les données à addLogo.php -->
|
||||||
<form method="POST" action="<?php echo $logoAModifier ? '../scripts/updateLogo.php' : '../scripts/addLogo.php'; ?>">
|
<form method="POST" action="<?php echo $logoAModifier ? '../scripts/updateLogo.php' : '../scripts/addLogo.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
<?php if ($logoAModifier): ?>
|
<?php if ($logoAModifier): ?>
|
||||||
<input type="hidden" name="id" value="<?php echo (int) $logoAModifier['id']; ?>">
|
<input type="hidden" name="id" value="<?php echo (int) $logoAModifier['id']; ?>">
|
||||||
@@ -376,6 +381,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
<div class="project-form-card">
|
<div class="project-form-card">
|
||||||
<!-- on envoie les données à updateSettings.php -->
|
<!-- on envoie les données à updateSettings.php -->
|
||||||
<form method="POST" action="../scripts/updateSettings.php">
|
<form method="POST" action="../scripts/updateSettings.php">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
<label>Titre (affiché en gras)</label>
|
<label>Titre (affiché en gras)</label>
|
||||||
<input type="text" name="notice_title" value="<?php echo htmlspecialchars($noticeTitle); ?>">
|
<input type="text" name="notice_title" value="<?php echo htmlspecialchars($noticeTitle); ?>">
|
||||||
@@ -406,6 +412,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
<div class="project-form-card">
|
<div class="project-form-card">
|
||||||
<!-- en envoie les données addUser.php -->
|
<!-- en envoie les données addUser.php -->
|
||||||
<form method="POST" action="../scripts/addUser.php">
|
<form method="POST" action="../scripts/addUser.php">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
<label>Nom du compte</label>
|
<label>Nom du compte</label>
|
||||||
<input type="text" name="user" required>
|
<input type="text" name="user" required>
|
||||||
|
|
||||||
@@ -440,7 +447,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
Télécharge une sauvegarde JSON de toutes les données du site
|
Télécharge une sauvegarde JSON de toutes les données du site
|
||||||
(projets, expériences, écoles, compétences, notification, utilisateurs).
|
(projets, expériences, écoles, compétences, notification, utilisateurs).
|
||||||
</p>
|
</p>
|
||||||
<a class="btn-submit admin-export-btn" href="../scripts/exportData.php">
|
<a class="btn-submit admin-export-btn" href="../scripts/exportData.php?csrf_token=<?php echo urlencode(csrfToken()); ?>">
|
||||||
<i class="fa-solid fa-download"></i> Exporter les données (JSON)
|
<i class="fa-solid fa-download"></i> Exporter les données (JSON)
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -454,6 +461,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
|||||||
</p>
|
</p>
|
||||||
<form method="POST" action="../scripts/importData.php" enctype="multipart/form-data"
|
<form method="POST" action="../scripts/importData.php" enctype="multipart/form-data"
|
||||||
onsubmit="return confirm('ATTENTION : les données actuelles du site vont être écrasées par celles du fichier. Continuer ?');">
|
onsubmit="return confirm('ATTENTION : les données actuelles du site vont être écrasées par celles du fichier. Continuer ?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
<input type="file" name="jsonfile" accept=".json,application/json" required>
|
<input type="file" name="jsonfile" accept=".json,application/json" required>
|
||||||
<input type="submit" value="Importer les données" class="btn-submit btn-delete">
|
<input type="submit" value="Importer les données" class="btn-submit btn-delete">
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+14
-7
@@ -8,10 +8,12 @@
|
|||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
// Types autorisés : liste blanche pour empêcher toute injection de nom de table
|
// Types autorisés : liste blanche pour empêcher toute injection de nom de table
|
||||||
|
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6) : on la
|
||||||
|
// joint juste pour le nom de fichier, jamais pour son contenu binaire)
|
||||||
$types = [
|
$types = [
|
||||||
'projet' => ['table' => 'projects', 'retour' => './projects.php', 'label' => 'Projet'],
|
'projet' => ['table' => 'projects', 'pdf' => 'project_pdfs', 'pdf_colonne' => 'project_id', 'retour' => './projects.php', 'label' => 'Projet'],
|
||||||
'experience' => ['table' => 'experiences', 'retour' => './xp.php', 'label' => 'Expérience'],
|
'experience' => ['table' => 'experiences', 'pdf' => 'experience_pdfs', 'pdf_colonne' => 'experience_id', 'retour' => './xp.php', 'label' => 'Expérience'],
|
||||||
'ecole' => ['table' => 'schools', 'retour' => './xp.php', 'label' => 'École'],
|
'ecole' => ['table' => 'schools', 'pdf' => 'school_pdfs', 'pdf_colonne' => 'school_id', 'retour' => './xp.php', 'label' => 'École'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$type = $_GET['type'] ?? '';
|
$type = $_GET['type'] ?? '';
|
||||||
@@ -28,15 +30,20 @@ $infos = $types[$type];
|
|||||||
// Récupération de l'élément demandé
|
// Récupération de l'élément demandé
|
||||||
// (pdf_data n'est volontairement pas sélectionné : le PDF est servi à part par viewPdf.php
|
// (pdf_data n'est volontairement pas sélectionné : le PDF est servi à part par viewPdf.php
|
||||||
// dans une <iframe>, inutile de charger son contenu binaire en mémoire ici)
|
// dans une <iframe>, inutile de charger son contenu binaire en mémoire ici)
|
||||||
$colonnesCommunes = 'id, title, status, tags, description, image_url, markdown, link_url, pdf_filename';
|
$colonnesCommunes = 'e.id, e.title, e.status, e.tags, e.description, e.image_url, e.markdown, e.link_url, p.pdf_filename';
|
||||||
$colonnesParType = [
|
$colonnesParType = [
|
||||||
'projet' => $colonnesCommunes,
|
'projet' => $colonnesCommunes,
|
||||||
'experience' => $colonnesCommunes . ', xptype, duration',
|
'experience' => $colonnesCommunes . ', e.xptype, e.duration',
|
||||||
'ecole' => $colonnesCommunes . ', duration',
|
'ecole' => $colonnesCommunes . ', e.duration',
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $dbh->prepare('SELECT ' . $colonnesParType[$type] . ' FROM `' . $infos['table'] . '` WHERE id = :id');
|
$stmt = $dbh->prepare(
|
||||||
|
'SELECT ' . $colonnesParType[$type] . '
|
||||||
|
FROM `' . $infos['table'] . '` e
|
||||||
|
LEFT JOIN `' . $infos['pdf'] . '` p ON p.`' . $infos['pdf_colonne'] . '` = e.id
|
||||||
|
WHERE e.id = :id'
|
||||||
|
);
|
||||||
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$element = $stmt->fetch(PDO::FETCH_ASSOC);
|
$element = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ require_once('../functions/listLogos.php');
|
|||||||
// Compétences, pas de saisie séparée (voir listeIconesCompetences)
|
// Compétences, pas de saisie séparée (voir listeIconesCompetences)
|
||||||
require_once('../functions/listSkills.php');
|
require_once('../functions/listSkills.php');
|
||||||
|
|
||||||
|
// Chiffres clés (bandeau de stats sous le hero) : nombre réel de projets/expériences
|
||||||
|
// en BDD, et nombre de technologies reconnaissables (réutilise listeIconesCompetences)
|
||||||
|
require_once('../functions/listStats.php');
|
||||||
|
$statsHome = recupererStatsHome();
|
||||||
|
$statsHome['technologies'] = count(listeIconesCompetences());
|
||||||
|
|
||||||
// Affichage de la notification (si elle est activée dans le panel Admin)
|
// Affichage de la notification (si elle est activée dans le panel Admin)
|
||||||
afficherNotice();
|
afficherNotice();
|
||||||
?>
|
?>
|
||||||
@@ -42,6 +48,27 @@ afficherNotice();
|
|||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Chiffres clés : n'affiche que les compteurs non nuls (pas de "0 projets" décevant
|
||||||
|
// si le site vient d'être initialisé)
|
||||||
|
$statsAffichables = array_filter([
|
||||||
|
'projets' => ['valeur' => $statsHome['projets'], 'label' => 'Projet' . ($statsHome['projets'] > 1 ? 's' : '') . ' réalisés'],
|
||||||
|
'experiences' => ['valeur' => $statsHome['experiences'], 'label' => 'Expérience' . ($statsHome['experiences'] > 1 ? 's' : '') . ' pro'],
|
||||||
|
'technologies' => ['valeur' => $statsHome['technologies'], 'label' => 'Technologies maîtrisées'],
|
||||||
|
], function ($stat) { return $stat['valeur'] > 0; });
|
||||||
|
?>
|
||||||
|
<?php if (!empty($statsAffichables)): ?>
|
||||||
|
<!-- Bandeau de chiffres clés : compteurs réels, calculés depuis la BDD -->
|
||||||
|
<section class="stats-strip">
|
||||||
|
<?php foreach ($statsAffichables as $stat): ?>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-number"><?php echo (int) $stat['valeur']; ?></span>
|
||||||
|
<span class="stat-label"><?php echo htmlspecialchars($stat['label']); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty(listeIconesCompetences())): ?>
|
<?php if (!empty(listeIconesCompetences())): ?>
|
||||||
<!-- Bandeau défilant des outils, dérivé de la page Compétences -->
|
<!-- Bandeau défilant des outils, dérivé de la page Compétences -->
|
||||||
<section class="logo-banner-section">
|
<section class="logo-banner-section">
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
require_once '../functions/databaseConnection.php';
|
require_once '../functions/databaseConnection.php';
|
||||||
require_once('../functions/sendLogs.php');
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
@@ -9,6 +9,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// La requête doit être un POST sinon on l'a refuse
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
|
|||||||
+18
-4
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
require_once '../functions/databaseConnection.php';
|
require_once '../functions/databaseConnection.php';
|
||||||
require_once('../functions/sendLogs.php');
|
require_once('../functions/sendLogs.php');
|
||||||
require_once('../functions/pdfUpload.php');
|
require_once('../functions/pdfUpload.php');
|
||||||
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
// tout de suite pour éviter un échec silencieux
|
// tout de suite pour éviter un échec silencieux
|
||||||
if (postDepasseLimite()) {
|
if (postDepasseLimite()) {
|
||||||
@@ -42,7 +45,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||||
try {
|
try {
|
||||||
// On prépare la requête SQL pour insérer le nouveau projet
|
// On prépare la requête SQL pour insérer le nouveau projet
|
||||||
$stmt = $dbh->prepare('INSERT INTO projects (title, status, tags, description, image_url, markdown, link_url, pdf_data, pdf_filename) VALUES (:title, :status, :tags, :description, :image_url, :markdown, :link_url, :pdf_data, :pdf_filename)');
|
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans project_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), pour que les listes de projets
|
||||||
|
// n'aient jamais à charger son contenu binaire en mémoire
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO projects (title, status, tags, description, image_url, markdown, link_url) VALUES (:title, :status, :tags, :description, :image_url, :markdown, :link_url)');
|
||||||
|
|
||||||
// On execute la requete préparée
|
// On execute la requete préparée
|
||||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
@@ -52,10 +58,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
|
||||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
|
// Si un PDF a été envoyé, on l'enregistre à part dans project_pdfs
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
$nouvelId = (int) $dbh->lastInsertId();
|
||||||
|
$stmtPdf = $dbh->prepare('INSERT INTO project_pdfs (project_id, pdf_filename, pdf_data) VALUES (:project_id, :pdf_filename, :pdf_data)');
|
||||||
|
$stmtPdf->bindValue(':project_id', $nouvelId, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
|
||||||
// Log Discord
|
// Log Discord
|
||||||
sendLog("Ajout du projet : " . $title);
|
sendLog("Ajout du projet : " . $title);
|
||||||
|
|
||||||
|
|||||||
+18
-4
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
require_once '../functions/databaseConnection.php';
|
require_once '../functions/databaseConnection.php';
|
||||||
require_once('../functions/sendLogs.php');
|
require_once('../functions/sendLogs.php');
|
||||||
require_once('../functions/pdfUpload.php');
|
require_once('../functions/pdfUpload.php');
|
||||||
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
// tout de suite pour éviter un échec silencieux
|
// tout de suite pour éviter un échec silencieux
|
||||||
if (postDepasseLimite()) {
|
if (postDepasseLimite()) {
|
||||||
@@ -42,7 +45,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (!empty($title) && !empty($description) && !empty($duration) && !empty($image_url)) {
|
if (!empty($title) && !empty($description) && !empty($duration) && !empty($image_url)) {
|
||||||
try {
|
try {
|
||||||
// On prépare la requête SQL pour insérer la nouvelle école
|
// On prépare la requête SQL pour insérer la nouvelle école
|
||||||
$stmt = $dbh->prepare('INSERT INTO schools (title, status, tags, description, duration, image_url, markdown, link_url, pdf_data, pdf_filename) VALUES (:title, :status, :tags, :description, :duration, :image_url, :markdown, :link_url, :pdf_data, :pdf_filename)');
|
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans school_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), pour que les listes d'écoles
|
||||||
|
// n'aient jamais à charger son contenu binaire en mémoire
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO schools (title, status, tags, description, duration, image_url, markdown, link_url) VALUES (:title, :status, :tags, :description, :duration, :image_url, :markdown, :link_url)');
|
||||||
|
|
||||||
// On execute la requete préparée
|
// On execute la requete préparée
|
||||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
@@ -53,10 +59,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
|
||||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
|
// Si un PDF a été envoyé, on l'enregistre à part dans school_pdfs
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
$nouvelId = (int) $dbh->lastInsertId();
|
||||||
|
$stmtPdf = $dbh->prepare('INSERT INTO school_pdfs (school_id, pdf_filename, pdf_data) VALUES (:school_id, :pdf_filename, :pdf_data)');
|
||||||
|
$stmtPdf->bindValue(':school_id', $nouvelId, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
|
||||||
// Log Discord
|
// Log Discord
|
||||||
sendLog("Ajout de l'école : " . $title);
|
sendLog("Ajout de l'école : " . $title);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
require_once '../functions/databaseConnection.php';
|
require_once '../functions/databaseConnection.php';
|
||||||
require_once('../functions/sendLogs.php');
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
@@ -9,6 +9,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// La requête doit être un POST sinon on l'a refuse
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// Ajout de connexion à la BDD
|
// Ajout de connexion à la BDD
|
||||||
require_once("../functions/databaseConnection.php");
|
require_once("../functions/databaseConnection.php");
|
||||||
// Ajout des logs Discord
|
// Ajout des logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Si le formulaire a été soumis
|
// Si le formulaire a été soumis
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||||
// Récupération des données du formulaire
|
// Récupération des données du formulaire
|
||||||
|
|||||||
+18
-4
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
require_once '../functions/databaseConnection.php';
|
require_once '../functions/databaseConnection.php';
|
||||||
require_once('../functions/sendLogs.php');
|
require_once('../functions/sendLogs.php');
|
||||||
require_once('../functions/pdfUpload.php');
|
require_once('../functions/pdfUpload.php');
|
||||||
@@ -10,6 +10,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
// tout de suite pour éviter un échec silencieux
|
// tout de suite pour éviter un échec silencieux
|
||||||
if (postDepasseLimite()) {
|
if (postDepasseLimite()) {
|
||||||
@@ -43,7 +46,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||||
try {
|
try {
|
||||||
// On prépare la requête SQL pour insérer la nouvelle experience
|
// On prépare la requête SQL pour insérer la nouvelle experience
|
||||||
$stmt = $dbh->prepare('INSERT INTO experiences (title, status, xptype, tags, description, duration, image_url, markdown, link_url, pdf_data, pdf_filename) VALUES (:title, :status, :xptype, :tags, :description, :duration, :image_url, :markdown, :link_url, :pdf_data, :pdf_filename)');
|
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans experience_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), pour que les listes d'expériences
|
||||||
|
// n'aient jamais à charger son contenu binaire en mémoire
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO experiences (title, status, xptype, tags, description, duration, image_url, markdown, link_url) VALUES (:title, :status, :xptype, :tags, :description, :duration, :image_url, :markdown, :link_url)');
|
||||||
|
|
||||||
// On execute la requete préparée
|
// On execute la requete préparée
|
||||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
@@ -55,10 +61,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
|
||||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
|
// Si un PDF a été envoyé, on l'enregistre à part dans experience_pdfs
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
$nouvelId = (int) $dbh->lastInsertId();
|
||||||
|
$stmtPdf = $dbh->prepare('INSERT INTO experience_pdfs (experience_id, pdf_filename, pdf_data) VALUES (:experience_id, :pdf_filename, :pdf_data)');
|
||||||
|
$stmtPdf->bindValue(':experience_id', $nouvelId, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
|
||||||
// Log Discord
|
// Log Discord
|
||||||
sendLog("Ajout de l'expérience : " . $title);
|
sendLog("Ajout de l'expérience : " . $title);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Verification de l'utilisateur voir si ces ids sont bon ou pas
|
// Verification de l'utilisateur voir si ces ids sont bon ou pas
|
||||||
|
|
||||||
// On démarre une session au cas où le script soit exécuté directement par URL
|
// On démarre une session au cas où le script soit exécuté directement par URL
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
|
||||||
// Ajout de connexion a la bdd
|
// Ajout de connexion a la bdd
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
@@ -38,6 +38,11 @@ if (!$user || !password_verify($password, $user['password'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Régénère l'ID de session à la connexion : empêche la fixation de session
|
||||||
|
// (un attaquant qui aurait imposé un ID de session à la victime avant sa
|
||||||
|
// connexion ne peut plus récupérer une session authentifiée avec cet ID)
|
||||||
|
session_regenerate_id(true);
|
||||||
|
|
||||||
// Si la connexion est ok, on entregistre l'utilisateur dans $_SESSION
|
// Si la connexion est ok, on entregistre l'utilisateur dans $_SESSION
|
||||||
$_SESSION['user_id'] = $user['id'];
|
$_SESSION['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
// Lorsque l'utilisateur est renvoyé à cette page/fonction, il est déconnecté
|
// Lorsque l'utilisateur est renvoyé à cette page/fonction, il est déconnecté
|
||||||
|
|
||||||
// Démarrage de session
|
// Démarrage de session (cookie durci)
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
|
||||||
// Ajout des logs Discord
|
// Ajout des logs Discord
|
||||||
require_once('../functions/sendLogs.php');
|
require_once('../functions/sendLogs.php');
|
||||||
@@ -21,6 +21,7 @@ session_destroy();
|
|||||||
// Si le captcha avait déjà été validé, on recrée une session propre
|
// Si le captcha avait déjà été validé, on recrée une session propre
|
||||||
// pour conserver uniquement cette information
|
// pour conserver uniquement cette information
|
||||||
if ($captchaAlreadyValid) {
|
if ($captchaAlreadyValid) {
|
||||||
|
configureSecureSessionCookies();
|
||||||
session_start();
|
session_start();
|
||||||
$_SESSION['captcha_valid'] = true;
|
$_SESSION['captcha_valid'] = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-10
@@ -3,7 +3,7 @@
|
|||||||
// Le fichier téléchargé peut ensuite être ré-importé via importData.php
|
// Le fichier téléchargé peut ensuite être ré-importé via importData.php
|
||||||
|
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -15,22 +15,32 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Liste blanche des tables et colonnes exportées (empêche toute injection via noms de colonnes)
|
// Liste blanche des tables et colonnes exportées (empêche toute injection via noms de colonnes)
|
||||||
|
// Depuis la migration Juillet 2026 (6), les PDF vivent dans des tables associées
|
||||||
|
// (project_pdfs, experience_pdfs, school_pdfs) plutôt que dans projects/experiences/schools :
|
||||||
|
// elles sont placées juste après leur table parente (l'ordre compte pour l'import, à cause
|
||||||
|
// des clés étrangères ON DELETE CASCADE)
|
||||||
$tables = [
|
$tables = [
|
||||||
'settings' => ['name', 'value'],
|
'settings' => ['name', 'value'],
|
||||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
|
||||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
'experience_pdfs' => ['experience_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
|
'school_pdfs' => ['school_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Colonnes binaires : le JSON ne peut pas contenir de binaire brut, on les encode en base64
|
// Colonnes binaires : le JSON ne peut pas contenir de binaire brut, on les encode en base64
|
||||||
// (+33% de taille mais nécessaire pour que le PDF survive à un export/import complet)
|
// (+33% de taille mais nécessaire pour que le PDF survive à un export/import complet)
|
||||||
$colonnesBinaires = [
|
$colonnesBinaires = [
|
||||||
'projects' => ['pdf_data'],
|
'project_pdfs' => ['pdf_data'],
|
||||||
'experiences' => ['pdf_data'],
|
'experience_pdfs' => ['pdf_data'],
|
||||||
'schools' => ['pdf_data'],
|
'school_pdfs' => ['pdf_data'],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Métadonnées de l'export (pratique pour s'y retrouver dans les sauvegardes)
|
// Métadonnées de l'export (pratique pour s'y retrouver dans les sauvegardes)
|
||||||
|
|||||||
+20
-11
@@ -3,7 +3,7 @@
|
|||||||
// ATTENTION : les données actuelles des tables présentes dans le fichier sont ÉCRASÉES
|
// ATTENTION : les données actuelles des tables présentes dans le fichier sont ÉCRASÉES
|
||||||
|
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -15,6 +15,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// La requête doit être un POST avec un fichier sinon on refuse
|
// La requête doit être un POST avec un fichier sinon on refuse
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['jsonfile']) || $_FILES['jsonfile']['error'] !== UPLOAD_ERR_OK) {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['jsonfile']) || $_FILES['jsonfile']['error'] !== UPLOAD_ERR_OK) {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
@@ -29,21 +32,27 @@ if (!is_array($donnees)) {
|
|||||||
die("Erreur : le fichier n'est pas un JSON valide.");
|
die("Erreur : le fichier n'est pas un JSON valide.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Même liste blanche que l'export : seules ces tables / colonnes peuvent être importées
|
// Même liste blanche que l'export : seules ces tables / colonnes peuvent être importées.
|
||||||
|
// L'ORDRE COMPTE : chaque table de PDF associée (project_pdfs...) doit être importée
|
||||||
|
// juste après sa table parente (projects...), car sa clé étrangère exige que la ligne
|
||||||
|
// parente existe déjà (voir migration Juillet 2026 (6))
|
||||||
$tables = [
|
$tables = [
|
||||||
'settings' => ['name', 'value'],
|
'settings' => ['name', 'value'],
|
||||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
|
||||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
'experience_pdfs' => ['experience_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
|
'school_pdfs' => ['school_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Colonnes binaires : encodées en base64 par exportData.php, à redécoder ici
|
// Colonnes binaires : encodées en base64 par exportData.php, à redécoder ici
|
||||||
$colonnesBinaires = [
|
$colonnesBinaires = [
|
||||||
'projects' => ['pdf_data'],
|
'project_pdfs' => ['pdf_data'],
|
||||||
'experiences' => ['pdf_data'],
|
'experience_pdfs' => ['pdf_data'],
|
||||||
'schools' => ['pdf_data'],
|
'school_pdfs' => ['pdf_data'],
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// Ajout de connexion à la BDD
|
// Ajout de connexion à la BDD
|
||||||
require_once("../functions/databaseConnection.php");
|
require_once("../functions/databaseConnection.php");
|
||||||
// Ajout des logs Discord
|
// Ajout des logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Récupération de l'ID passé en paramètre GET
|
// Récupération de l'ID passé en paramètre GET
|
||||||
$id = $_GET["id"];
|
$id = $_GET["id"];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
require_once('../../idDiscord.php');
|
require_once('../../idDiscord.php');
|
||||||
|
|
||||||
// Ajout des logs Discord
|
// Ajout des logs Discord
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
require_once('../functions/sendLogs.php');
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
// Envoi de formulaire de contact via WebHook discord
|
// Envoi de formulaire de contact via WebHook discord
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||||
empty($_POST['name']) ||
|
empty($_POST['name']) ||
|
||||||
|
|||||||
+29
-13
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||||
if (postDepasseLimite()) {
|
if (postDepasseLimite()) {
|
||||||
@@ -61,15 +64,13 @@ if (is_array($pdfResultat)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try { // On essaie d'executer une requête qui est préparée
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
$sql = '
|
// Le PDF (pdf_data) n'est plus stocké dans projects : il vit dans project_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
UPDATE projects
|
UPDATE projects
|
||||||
SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url';
|
SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||||
if ($updatePdf) {
|
WHERE id = :id
|
||||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
');
|
||||||
}
|
|
||||||
$sql .= ' WHERE id = :id';
|
|
||||||
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||||
@@ -77,12 +78,27 @@ try { // On essaie d'executer une requête qui est préparée
|
|||||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||||
if ($updatePdf) {
|
|
||||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
|
||||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
|
||||||
}
|
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
|
||||||
|
if ($updatePdf) {
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = project_id)
|
||||||
|
$stmtPdf = $dbh->prepare('
|
||||||
|
INSERT INTO project_pdfs (project_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||||
|
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||||
|
');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
} else {
|
||||||
|
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||||
|
$stmtPdf = $dbh->prepare('DELETE FROM project_pdfs WHERE project_id = :id');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur lors de la modification du projet :" . $e->getMessage());
|
die("Erreur lors de la modification du projet :" . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-13
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||||
if (postDepasseLimite()) {
|
if (postDepasseLimite()) {
|
||||||
@@ -62,15 +65,13 @@ if (is_array($pdfResultat)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try { // On essaie d'executer une requête qui est préparée
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
$sql = '
|
// Le PDF (pdf_data) n'est plus stocké dans schools : il vit dans school_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
UPDATE schools
|
UPDATE schools
|
||||||
SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url';
|
SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||||
if ($updatePdf) {
|
WHERE id = :id
|
||||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
');
|
||||||
}
|
|
||||||
$sql .= ' WHERE id = :id';
|
|
||||||
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||||
@@ -79,12 +80,27 @@ try { // On essaie d'executer une requête qui est préparée
|
|||||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||||
if ($updatePdf) {
|
|
||||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
|
||||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
|
||||||
}
|
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
|
||||||
|
if ($updatePdf) {
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = school_id)
|
||||||
|
$stmtPdf = $dbh->prepare('
|
||||||
|
INSERT INTO school_pdfs (school_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||||
|
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||||
|
');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
} else {
|
||||||
|
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||||
|
$stmtPdf = $dbh->prepare('DELETE FROM school_pdfs WHERE school_id = :id');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur lors de la modification de l'école :" . $e->getMessage());
|
die("Erreur lors de la modification de l'école :" . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// La requête doit être un POST sinon on l'a refuse
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -12,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||||
empty($_POST['title']) ||
|
empty($_POST['title']) ||
|
||||||
|
|||||||
+29
-13
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
// On ajoute les logs Discord
|
// On ajoute les logs Discord
|
||||||
@@ -13,6 +13,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||||
if (postDepasseLimite()) {
|
if (postDepasseLimite()) {
|
||||||
@@ -65,15 +68,13 @@ if (is_array($pdfResultat)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try { // On essaie d'executer une requête qui est préparée
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
$sql = '
|
// Le PDF (pdf_data) n'est plus stocké dans experiences : il vit dans experience_pdfs
|
||||||
|
// (table associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
UPDATE experiences
|
UPDATE experiences
|
||||||
SET title = :title, status = :status, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url';
|
SET title = :title, status = :status, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||||
if ($updatePdf) {
|
WHERE id = :id
|
||||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
');
|
||||||
}
|
|
||||||
$sql .= ' WHERE id = :id';
|
|
||||||
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
||||||
@@ -83,12 +84,27 @@ try { // On essaie d'executer une requête qui est préparée
|
|||||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||||
if ($updatePdf) {
|
|
||||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
|
||||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
|
||||||
}
|
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
|
||||||
|
if ($updatePdf) {
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = experience_id)
|
||||||
|
$stmtPdf = $dbh->prepare('
|
||||||
|
INSERT INTO experience_pdfs (experience_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||||
|
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||||
|
');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
} else {
|
||||||
|
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||||
|
$stmtPdf = $dbh->prepare('DELETE FROM experience_pdfs WHERE experience_id = :id');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur lors de la modification de l experience :" . $e->getMessage());
|
die("Erreur lors de la modification de l experience :" . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -6,10 +6,12 @@
|
|||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
// Liste blanche des types autorisés : empêche toute injection de nom de table
|
// Liste blanche des types autorisés : empêche toute injection de nom de table
|
||||||
|
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6),
|
||||||
|
// pour ne plus jamais charger son contenu binaire lors des listes de projets/xp/écoles)
|
||||||
$tables = [
|
$tables = [
|
||||||
'projet' => 'projects',
|
'projet' => ['pdf' => 'project_pdfs', 'colonne' => 'project_id'],
|
||||||
'experience' => 'experiences',
|
'experience' => ['pdf' => 'experience_pdfs', 'colonne' => 'experience_id'],
|
||||||
'ecole' => 'schools',
|
'ecole' => ['pdf' => 'school_pdfs', 'colonne' => 'school_id'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$type = $_GET['type'] ?? '';
|
$type = $_GET['type'] ?? '';
|
||||||
@@ -21,7 +23,7 @@ if (!isset($tables[$type]) || !is_numeric($id)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $dbh->prepare('SELECT pdf_data, pdf_filename FROM `' . $tables[$type] . '` WHERE id = :id');
|
$stmt = $dbh->prepare('SELECT pdf_data, pdf_filename FROM `' . $tables[$type]['pdf'] . '` WHERE `' . $tables[$type]['colonne'] . '` = :id');
|
||||||
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|||||||
@@ -517,6 +517,44 @@ img {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Bandeau de chiffres clés (page d'accueil) ---- */
|
||||||
|
.stats-strip {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px 70px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 18px 34px;
|
||||||
|
min-width: 140px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: clamp(28px, 4vw, 38px);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user