Files
Portfolio-Esteban/pages/details.php
T
2026-07-02 15:13:54 +02:00

160 lines
6.2 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
$types = [
'projet' => ['table' => 'projects', 'retour' => './projects.php', 'label' => 'Projet'],
'experience' => ['table' => 'experiences', 'retour' => './xp.php', 'label' => 'Expérience'],
'ecole' => ['table' => 'schools', 'retour' => './xp.php', 'label' => 'École'],
];
$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 = 'id, title, status, tags, description, image_url, markdown, link_url, pdf_filename';
$colonnesParType = [
'projet' => $colonnesCommunes,
'experience' => $colonnesCommunes . ', xptype, duration',
'ecole' => $colonnesCommunes . ', duration',
];
try {
$stmt = $dbh->prepare('SELECT ' . $colonnesParType[$type] . ' FROM `' . $infos['table'] . '` WHERE 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 if (!empty($image)): ?>
<div class="details-banner project-banner">
<img src="<?php echo $image; ?>" alt="Image : <?php echo $titre; ?>">
<?php if (!empty($status)): ?>
<div class="project-state"><?php echo $status; ?></div>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="details-content">
<span class="details-type"><?php echo $infos['label']; ?></span>
<h1><?php echo $titre; ?></h1>
<!-- Métadonnées : type / durée si renseignés -->
<?php if (!empty($xptype) || !empty($duration)): ?>
<p class="details-meta">
<?php if (!empty($xptype)): ?><span><i class="fa-solid fa-tag"></i> <?php echo $xptype; ?></span><?php endif; ?>
<?php if (!empty($duration)): ?><span><i class="fa-solid fa-clock"></i> <?php echo $duration; ?></span><?php endif; ?>
</p>
<?php endif; ?>
<?php if (!empty($tags)): ?>
<div class="project-tags">
<?php foreach ($tags as $tag): ?>
<span class="tag"><?php echo htmlspecialchars($tag); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($hasMarkdown): ?>
<!-- Le markdown brut est placé ici (échappé), puis rendu en HTML par le script plus bas -->
<div id="markdown-source" hidden><?php echo htmlspecialchars($element['markdown']); ?></div>
<div class="markdown-content" id="markdown-target">
<p>Chargement du contenu...</p>
</div>
<?php elseif ($hasPdf): ?>
<!-- Mini lecteur PDF : le fichier est servi par viewPdf.php depuis la base de données -->
<div class="pdf-viewer-wrap">
<div class="pdf-viewer-toolbar">
<span><i class="fa-solid fa-file-pdf"></i> <?php echo htmlspecialchars($element['pdf_filename']); ?></span>
<a class="btn btn-ghost pdf-download-btn" href="../scripts/viewPdf.php?type=<?php echo $type; ?>&id=<?php echo (int) $id; ?>&download=1" download>
<i class="fa-solid fa-download"></i> Télécharger
</a>
</div>
<iframe
class="pdf-viewer"
src="../scripts/viewPdf.php?type=<?php echo $type; ?>&id=<?php echo (int) $id; ?>"
title="Aperçu PDF : <?php echo $titre; ?>"
></iframe>
</div>
<?php endif; ?>
</div>
</article>
</div>
<?php if ($hasMarkdown): ?>
<!-- marked.js : convertit le markdown en HTML / DOMPurify : nettoie le HTML généré (sécurité) -->
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
<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>
<?php endif; ?>
<?php
// Ajout du footer
require_once('../elements/footer.php'); ?>