This commit is contained in:
Esteban
2026-07-02 15:13:54 +02:00
parent a019dcb00d
commit 07bae3eff3
38 changed files with 2039 additions and 226 deletions
+23 -4
View File
@@ -11,7 +11,9 @@ function listeProjets() {
// Essayer
try {
// Requête SQL qui récupère titre, tags, description, image_url
$stmt = $dbh->prepare('SELECT id, title, status, tags, description, image_url FROM projects ORDER BY id DESC');
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
// même si la migration SQL n'a pas encore été exécutée)
$stmt = $dbh->prepare('SELECT * FROM projects ORDER BY id DESC');
// Execution de la requête SQL
$stmt->execute();
// Retourne toutes les données collectés dans un ARRAY
@@ -27,7 +29,7 @@ function recupererProjetParId($id) {
global $dbh;
try {
$stmt = $dbh->prepare('SELECT id, title, status, tags, description, image_url FROM projects WHERE id = :id');
$stmt = $dbh->prepare('SELECT * FROM projects WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
@@ -50,10 +52,14 @@ function afficherProjets() {
// Pour chaque projet dans le ARRAY
foreach ($projets as $projet) {
// On récupère le titre, la description et l'image et le tableau avec les tags
$id = (int) $projet['id'];
$titre = htmlspecialchars($projet['title']);
$description = nl2br(htmlspecialchars($projet['description']));
$image = htmlspecialchars($projet['image_url']);
$status = htmlspecialchars($projet['status'] ?? '');
$markdown = trim($projet['markdown'] ?? '');
$lien = trim($projet['link_url'] ?? '');
$pdf = trim($projet['pdf_filename'] ?? '');
$tags = [];
// S'il y a des tags alors on les sépares grâce à la virgule que l'user à ajouté dans le champs (tags1,tags2,etcc...)
@@ -88,8 +94,21 @@ function afficherProjets() {
echo '</div>';
}
// Ajout de la description et fin des classes
// Ajout de la description et fin des classes
echo '<p>' . $description . '</p>';
// Indicateur de page détaillée : markdown > PDF > lien externe > rien
// Le lien rend toute la carte cliquable (voir CSS .card-link)
if ($markdown !== '') {
echo '<a class="card-link" href="./details.php?type=projet&id=' . $id . '"><i class="fa-solid fa-file-lines"></i> Voir la page détaillée</a>';
} elseif ($pdf !== '') {
echo '<a class="card-link" href="./details.php?type=projet&id=' . $id . '"><i class="fa-solid fa-file-pdf"></i> Voir le PDF</a>';
} elseif ($lien !== '') {
echo '<a class="card-link" href="' . $lien . '" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-arrow-up-right-from-square"></i> Voir le lien externe</a>';
} else {
echo '<span class="card-link card-link-none"><i class="fa-solid fa-eye-slash"></i> Pas de page détaillée</span>';
}
echo '</div>';
echo '</article>';
@@ -144,7 +163,7 @@ function afficherProjetsAdmin() {
// Boutons qui vont permettrent la modification / Suppresion
echo '<div class="admin-actions">';
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '">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 '</div>';
+21 -3
View File
@@ -10,7 +10,9 @@ function listeEcoles() {
global $dbh;
try {
$stmt = $dbh->prepare('SELECT id, title, status, tags, description, duration, image_url FROM schools ORDER BY id DESC');
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
// même si la migration SQL n'a pas encore été exécutée)
$stmt = $dbh->prepare('SELECT * FROM schools ORDER BY id DESC');
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
@@ -23,7 +25,7 @@ function recupererEcoleParId($id) {
global $dbh;
try {
$stmt = $dbh->prepare('SELECT id, title, status, tags, description, duration, image_url FROM schools WHERE id = :id');
$stmt = $dbh->prepare('SELECT * FROM schools WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
@@ -40,6 +42,9 @@ function afficherEcoleCard($ecole, $admin = false) {
$duration = htmlspecialchars($ecole['duration']);
$image = htmlspecialchars($ecole['image_url']);
$status = htmlspecialchars($ecole['status'] ?? '');
$markdown = trim($ecole['markdown'] ?? '');
$lien = trim($ecole['link_url'] ?? '');
$pdf = trim($ecole['pdf_filename'] ?? '');
$tags = [];
// S'il y a des tags alors on les sépares grâce à la virgule (tags1,tags2,etc...)
@@ -76,10 +81,23 @@ function afficherEcoleCard($ecole, $admin = false) {
echo '<p>' . $description . '</p>';
// Indicateur de page détaillée (uniquement sur la page publique) : markdown > PDF > lien externe > rien
if (!$admin) {
if ($markdown !== '') {
echo '<a class="card-link" href="./details.php?type=ecole&id=' . $id . '"><i class="fa-solid fa-file-lines"></i> Voir la page détaillée</a>';
} elseif ($pdf !== '') {
echo '<a class="card-link" href="./details.php?type=ecole&id=' . $id . '"><i class="fa-solid fa-file-pdf"></i> Voir le PDF</a>';
} elseif ($lien !== '') {
echo '<a class="card-link" href="' . $lien . '" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-arrow-up-right-from-square"></i> Voir le lien externe</a>';
} else {
echo '<span class="card-link card-link-none"><i class="fa-solid fa-eye-slash"></i> Pas de page détaillée</span>';
}
}
// Boutons de modification / suppression pour le panel admin
if ($admin) {
echo '<div class="admin-actions">';
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_school=' . $id . '">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 '</div>';
}
+1 -1
View File
@@ -61,7 +61,7 @@ function afficherSkillCard($skill, $admin = false) {
// Boutons de modification / suppression pour le panel admin
if ($admin) {
echo '<div class="admin-actions">';
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_skill=' . $id . '">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 '</div>';
}
+23 -4
View File
@@ -14,7 +14,9 @@ function listeXP() {
// Essayer
try {
// Requête SQL qui récupère titre, type, tags, description, durée, image_url
$stmt = $dbh->prepare('SELECT id, title, status, xptype, tags, description, duration, image_url FROM experiences ORDER BY id DESC');
// SELECT * : les colonnes markdown / link_url sont optionnelles (la page fonctionne
// même si la migration SQL n'a pas encore été exécutée)
$stmt = $dbh->prepare('SELECT * FROM experiences ORDER BY id DESC');
// Execution de la requête SQL
$stmt->execute();
// Retourne toutes les données collectés dans un ARRAY
@@ -31,7 +33,7 @@ function recupererXPParId($id) {
try {
// Préparation de la requête préparée
$stmt = $dbh->prepare('SELECT id, title, status, xptype, tags, description, duration, image_url FROM experiences WHERE id = :id');
$stmt = $dbh->prepare('SELECT * FROM experiences WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
@@ -58,12 +60,16 @@ function afficherXP() {
// Pour chaque experience dans le ARRAY
foreach ($experiences as $experience) {
// On récupère le titre, le type, la description, la durée et l'image et le tableau avec les tags
$id = (int) $experience['id'];
$titre = htmlspecialchars($experience['title']);
$xptype = htmlspecialchars($experience['xptype']);
$description = nl2br(htmlspecialchars($experience['description']));
$duration = htmlspecialchars($experience['duration']);
$image = htmlspecialchars($experience['image_url']);
$status = htmlspecialchars($experience['status'] ?? '');
$markdown = trim($experience['markdown'] ?? '');
$lien = trim($experience['link_url'] ?? '');
$pdf = trim($experience['pdf_filename'] ?? '');
$tags = [];
@@ -107,8 +113,21 @@ function afficherXP() {
}
// Ajout de la description et fin des classes
// Ajout de la description et fin des classes
echo '<p>' . $description . '</p>';
// Indicateur de page détaillée : markdown > PDF > lien externe > rien
// Le lien rend toute la carte cliquable (voir CSS .card-link)
if ($markdown !== '') {
echo '<a class="card-link" href="./details.php?type=experience&id=' . $id . '"><i class="fa-solid fa-file-lines"></i> Voir la page détaillée</a>';
} elseif ($pdf !== '') {
echo '<a class="card-link" href="./details.php?type=experience&id=' . $id . '"><i class="fa-solid fa-file-pdf"></i> Voir le PDF</a>';
} elseif ($lien !== '') {
echo '<a class="card-link" href="' . $lien . '" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-arrow-up-right-from-square"></i> Voir le lien externe</a>';
} else {
echo '<span class="card-link card-link-none"><i class="fa-solid fa-eye-slash"></i> Pas de page détaillée</span>';
}
echo '</div>';
@@ -175,7 +194,7 @@ function afficherXPAdmin() {
// Boutons qui vont permettrent la modification / Suppresion
echo '<div class="admin-actions">';
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '">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 '</div>';
+68
View File
@@ -0,0 +1,68 @@
<?php
// Fonction utilitaire de validation/lecture d'un PDF envoyé par formulaire (drag & drop ou clic)
// Utilisée par les scripts d'ajout/modification de projets, expériences et écoles
// Taille max acceptée par l'application : à ajuster avec upload_max_filesize / post_max_size
// (php.ini) et max_allowed_packet (MySQL) si besoin d'accepter des PDF plus lourds
define('PDF_MAX_SIZE', 15 * 1024 * 1024); // 15 Mo
// Détecte le cas où le fichier envoyé dépasse post_max_size (php.ini)
// Dans ce cas PHP vide $_POST et $_FILES AVANT même d'exécuter le script : sans cette
// détection, le formulaire échoue silencieusement (aucun champ, aucune erreur visible)
function postDepasseLimite() {
$contentLength = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
return $_SERVER['REQUEST_METHOD'] === 'POST' && $contentLength > 0 && empty($_POST) && empty($_FILES);
}
// Message d'erreur explicite à afficher quand postDepasseLimite() est vrai
function messageDepassementPost() {
return "Le fichier envoyé est trop volumineux pour le serveur (limite actuelle : "
. ini_get('post_max_size') . "). Réduis la taille du PDF, ou augmente "
. "post_max_size et upload_max_filesize côté serveur.";
}
// Vérifie et lit un fichier PDF envoyé en upload ($_FILES)
// Retourne :
// - null si aucun fichier n'a été envoyé (ce n'est pas une erreur)
// - une chaîne de texte si le fichier est invalide (message d'erreur à afficher)
// - ['filename' => ..., 'data' => ...] si le PDF est valide
function lireUploadPdf($champ = 'pdf') {
if (!isset($_FILES[$champ]) || $_FILES[$champ]['error'] === UPLOAD_ERR_NO_FILE) {
return null;
}
if ($_FILES[$champ]['error'] !== UPLOAD_ERR_OK) {
return "Erreur lors de l'envoi du PDF (code " . $_FILES[$champ]['error'] . ").";
}
if ($_FILES[$champ]['size'] > PDF_MAX_SIZE) {
return "Le PDF dépasse la taille maximale autorisée (15 Mo).";
}
// Vérification de l'extension
$extension = strtolower(pathinfo($_FILES[$champ]['name'], PATHINFO_EXTENSION));
if ($extension !== 'pdf') {
return "Le fichier doit être un PDF (extension .pdf).";
}
// Vérification du type réel du fichier (pas seulement l'extension, pour éviter
// qu'un fichier malveillant renommé en .pdf soit accepté)
// (pas de finfo_close : déprécié depuis PHP 8.5, la ressource se libère toute seule)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES[$champ]['tmp_name']);
if ($mime !== 'application/pdf') {
return "Le fichier envoyé n'est pas un PDF valide.";
}
$contenu = file_get_contents($_FILES[$champ]['tmp_name']);
if ($contenu === false) {
return "Impossible de lire le fichier envoyé.";
}
return [
'filename' => basename($_FILES[$champ]['name']),
'data' => $contenu
];
}
+49 -4
View File
@@ -1,21 +1,36 @@
<?php
// On cache les webhooks discord => Correction de faille de sécurité
require_once('../../idDiscord.php');
// __DIR__ = chemin absolu du dossier functions : le require fonctionne ainsi quel que soit
// le dossier depuis lequel le script est appelé (pages/, scripts/, racine...)
require_once(__DIR__ . '/../../idDiscord.php');
// EXEMPLE de a quoi ressemble idDiscord :
// define('ID_DISCORD1', 'https://discord.com/api/webhooks/URL_WEBHOOK1');
// define('ID_DISCORD2', 'https://discord.com/api/webhooks/URL_WEBHOOK2');
// Fonction qui va ENVOYER des logs sur le webhook discord
function sendLog($action, $user="") {
function sendLog($action, $user = "") {
// Si le webhook n'est pas configuré on ne bloque pas le site
if (!defined('ID_DISCORD1') || ID_DISCORD1 == '') {
return false;
}
$webhook = ID_DISCORD1; // URL du webhook Discord
// Si l'utilisateur n'est pas précisé on prend celui de la session (connecté)
if ($user == "" && isset($_SESSION['username'])) {
$user = $_SESSION['username'];
}
$contenuLog = "**Nouvelle log du portfolio**\n\n";
$contenuLog .= "**Action :** " . $action . "\n";
if ($user != "") {
$contenuLog .= "**Utilisateur :** " . $user . "\n";
} else {
$contenuLog .= "**Utilisateur :** non défini". "\n";
$contenuLog .= "**Utilisateur :** non défini" . "\n";
}
$contenuLog .= "**IP :** " . ($_SERVER['REMOTE_ADDR'] ?? 'inconnue') . "\n";
$contenuLog .= "**Date :** " . date('d/m/Y H:i:s');
$data = [
"content" => $contenuLog
@@ -28,6 +43,36 @@ function sendLog($action, $user="") {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataJson);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Timeouts courts : si Discord est lent ou down, le site ne doit pas être ralenti
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 800);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1500);
// CORRECTION DU BUG : curl_exec() n'était jamais appelé, la requête n'était
// donc jamais envoyée à Discord. C'est pour ça que les logs n'ont jamais marché.
// (pas de curl_close : déprécié depuis PHP 8.5, la ressource se libère toute seule)
curl_exec($ch);
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Discord renvoie 204 quand le message est bien envoyé
return $codeHttp == 204;
}
//
// Log de visite de page : appelée par le header sur toutes les pages
// Anti-spam : Discord limite à ~30 messages/minute par webhook, donc une même page
// n'est loguée qu'une fois par minute et par visiteur (stocké dans sa session)
function sendPageLog() {
$page = basename($_SERVER['PHP_SELF'] ?? 'inconnue');
if (!isset($_SESSION['pages_log'])) {
$_SESSION['pages_log'] = [];
}
$maintenant = time();
if (isset($_SESSION['pages_log'][$page]) && ($maintenant - $_SESSION['pages_log'][$page]) < 60) {
return; // Page déjà loguée il y a moins d'une minute pour ce visiteur
}
$_SESSION['pages_log'][$page] = $maintenant;
sendLog("Visite de la page " . $page);
}