195 lines
7.4 KiB
PHP
195 lines
7.4 KiB
PHP
<?php
|
|
|
|
// Page de détails d'un projet / d'une expérience / d'une école
|
|
// Le contenu markdown (stocké en BDD, éditable depuis le panel admin) est rendu
|
|
// côté navigateur avec marked.js puis nettoyé avec DOMPurify (sécurité XSS)
|
|
|
|
// Connexion BDD (avant le header car on doit pouvoir rediriger si l'élément n'existe pas)
|
|
require_once('../functions/databaseConnection.php');
|
|
|
|
// Types autorisés : liste blanche pour empêcher toute injection de nom de table
|
|
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6) : on la
|
|
// joint juste pour le nom de fichier, jamais pour son contenu binaire)
|
|
$types = [
|
|
'projet' => ['table' => 'projects', 'pdf' => 'project_pdfs', 'pdf_colonne' => 'project_id', 'retour' => './projects.php', 'label' => 'Projet'],
|
|
'experience' => ['table' => 'experiences', 'pdf' => 'experience_pdfs', 'pdf_colonne' => 'experience_id', 'retour' => './xp.php', 'label' => 'Expérience'],
|
|
'ecole' => ['table' => 'schools', 'pdf' => 'school_pdfs', 'pdf_colonne' => 'school_id', 'retour' => './xp.php', 'label' => 'École'],
|
|
];
|
|
|
|
$type = $_GET['type'] ?? '';
|
|
$id = $_GET['id'] ?? '';
|
|
|
|
// Type inconnu ou id invalide : retour à l'accueil
|
|
if (!isset($types[$type]) || !is_numeric($id)) {
|
|
header('Location: ./home.php');
|
|
exit;
|
|
}
|
|
|
|
$infos = $types[$type];
|
|
|
|
// Récupération de l'élément demandé
|
|
// (pdf_data n'est volontairement pas sélectionné : le PDF est servi à part par viewPdf.php
|
|
// dans une <iframe>, inutile de charger son contenu binaire en mémoire ici)
|
|
$colonnesCommunes = 'e.id, e.title, e.status, e.tags, e.description, e.image_url, e.markdown, e.link_url, p.pdf_filename';
|
|
$colonnesParType = [
|
|
'projet' => $colonnesCommunes,
|
|
'experience' => $colonnesCommunes . ', e.xptype, e.duration',
|
|
'ecole' => $colonnesCommunes . ', e.duration',
|
|
];
|
|
|
|
try {
|
|
$stmt = $dbh->prepare(
|
|
'SELECT ' . $colonnesParType[$type] . '
|
|
FROM `' . $infos['table'] . '` e
|
|
LEFT JOIN `' . $infos['pdf'] . '` p ON p.`' . $infos['pdf_colonne'] . '` = e.id
|
|
WHERE e.id = :id'
|
|
);
|
|
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
$element = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
} catch (PDOException $e) {
|
|
$element = false;
|
|
}
|
|
|
|
$hasMarkdown = $element && trim($element['markdown'] ?? '') !== '';
|
|
$hasPdf = $element && trim($element['pdf_filename'] ?? '') !== '';
|
|
|
|
// Élément inexistant, ou sans markdown ni PDF : retour à la liste
|
|
if (!$element || (!$hasMarkdown && !$hasPdf)) {
|
|
header('Location: ' . $infos['retour']);
|
|
exit;
|
|
}
|
|
|
|
// Titre de l'onglet
|
|
$pageTitle = $element['title'];
|
|
|
|
// On ajoute le header
|
|
require_once('../elements/header.php');
|
|
|
|
// Préparation des données pour l'affichage
|
|
$titre = htmlspecialchars($element['title']);
|
|
$image = htmlspecialchars($element['image_url'] ?? '');
|
|
$status = htmlspecialchars($element['status'] ?? '');
|
|
$duration = htmlspecialchars($element['duration'] ?? '');
|
|
$xptype = htmlspecialchars($element['xptype'] ?? '');
|
|
$tags = [];
|
|
if (!empty($element['tags'])) {
|
|
$tags = array_filter(array_map('trim', explode(',', $element['tags'])));
|
|
}
|
|
?>
|
|
|
|
<div class="details-page">
|
|
|
|
<!-- Bouton retour vers la liste -->
|
|
<a class="btn btn-ghost details-back" href="<?php echo $infos['retour']; ?>">
|
|
<i class="fa-solid fa-arrow-left"></i> Retour
|
|
</a>
|
|
|
|
<article class="details-card">
|
|
|
|
<?php
|
|
// Bandeau d'image, seulement si une image est renseignée pour cet élément
|
|
if (!empty($image)) {
|
|
echo '<div class="details-banner project-banner">';
|
|
echo '<img src="' . $image . '" alt="Image : ' . $titre . '">';
|
|
|
|
// Pastille d'état (En cours, Terminé...) posée par-dessus l'image
|
|
if (!empty($status)) {
|
|
echo '<div class="project-state">' . $status . '</div>';
|
|
}
|
|
|
|
echo '</div>';
|
|
}
|
|
?>
|
|
|
|
<div class="details-content">
|
|
|
|
<span class="details-type"><?php echo $infos['label']; ?></span>
|
|
<h1><?php echo $titre; ?></h1>
|
|
|
|
<?php
|
|
// Métadonnées : type d'expérience et durée, affichés seulement si renseignés
|
|
if (!empty($xptype) || !empty($duration)) {
|
|
echo '<p class="details-meta">';
|
|
|
|
if (!empty($xptype)) {
|
|
echo '<span><i class="fa-solid fa-tag"></i> ' . $xptype . '</span>';
|
|
}
|
|
|
|
if (!empty($duration)) {
|
|
echo '<span><i class="fa-solid fa-clock"></i> ' . $duration . '</span>';
|
|
}
|
|
|
|
echo '</p>';
|
|
}
|
|
|
|
// Étiquettes (technologies, mots-clés), une par tag séparé par une virgule en BDD
|
|
if (!empty($tags)) {
|
|
echo '<div class="project-tags">';
|
|
|
|
foreach ($tags as $tag) {
|
|
echo '<span class="tag">' . htmlspecialchars($tag) . '</span>';
|
|
}
|
|
|
|
echo '</div>';
|
|
}
|
|
?>
|
|
|
|
<?php
|
|
// Deux contenus possibles, jamais les deux en même temps : soit un texte en
|
|
// markdown, soit un PDF. Le markdown est prioritaire quand les deux existent.
|
|
if ($hasMarkdown) {
|
|
// Le markdown brut est placé ici (échappé), puis rendu en HTML par le script plus bas
|
|
echo '<div id="markdown-source" hidden>' . htmlspecialchars($element['markdown']) . '</div>';
|
|
echo '<div class="markdown-content" id="markdown-target">';
|
|
echo '<p>Chargement du contenu...</p>';
|
|
echo '</div>';
|
|
} elseif ($hasPdf) {
|
|
// Mini lecteur PDF : le fichier est servi par viewPdf.php depuis la base de données
|
|
$lienPdf = '../scripts/viewPdf.php?type=' . $type . '&id=' . (int) $id;
|
|
|
|
echo '<div class="pdf-viewer-wrap">';
|
|
echo '<div class="pdf-viewer-toolbar">';
|
|
echo '<span><i class="fa-solid fa-file-pdf"></i> ' . htmlspecialchars($element['pdf_filename']) . '</span>';
|
|
echo '<a class="btn btn-ghost pdf-download-btn" href="' . $lienPdf . '&download=1" download>';
|
|
echo '<i class="fa-solid fa-download"></i> Télécharger';
|
|
echo '</a>';
|
|
echo '</div>';
|
|
echo '<iframe class="pdf-viewer" src="' . $lienPdf . '" title="Aperçu PDF : ' . $titre . '"></iframe>';
|
|
echo '</div>';
|
|
}
|
|
?>
|
|
|
|
</div>
|
|
|
|
</article>
|
|
|
|
</div>
|
|
|
|
<?php
|
|
// Ces deux librairies ne sont chargées que sur les pages qui ont réellement du markdown :
|
|
// marked.js convertit le markdown en HTML, DOMPurify nettoie le HTML généré (sécurité XSS)
|
|
if ($hasMarkdown) {
|
|
echo '<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>';
|
|
echo '<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>';
|
|
|
|
// <<<'JS' ... JS; = "nowdoc" : un bloc de texte sur plusieurs lignes, recopié tel quel.
|
|
// Pratique ici pour ne pas avoir à échapper les guillemets du JavaScript.
|
|
echo <<<'JS'
|
|
<script>
|
|
(function () {
|
|
var source = document.getElementById('markdown-source');
|
|
var cible = document.getElementById('markdown-target');
|
|
// textContent décode automatiquement les entités HTML => on récupère le markdown d'origine
|
|
var markdown = source.textContent;
|
|
cible.innerHTML = DOMPurify.sanitize(marked.parse(markdown, { breaks: true }));
|
|
})();
|
|
</script>
|
|
JS;
|
|
}
|
|
?>
|
|
|
|
<?php
|
|
// Ajout du footer
|
|
require_once('../elements/footer.php'); ?>
|