Dofus
This commit is contained in:
+10
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Ajout des logs Discord (chemin via __DIR__ car captcha.php est à la racine du site)
|
||||
require_once(__DIR__ . '/functions/sendLogs.php');
|
||||
|
||||
// Recharger une nouvelle série si demandé
|
||||
if (isset($_GET['reload']) && $_GET['reload'] === '1') {
|
||||
unset($_SESSION['captcha_round']);
|
||||
@@ -311,13 +314,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (($round['attempts_left'] ?? 0) <= 0) {
|
||||
$locked = true;
|
||||
$error = 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.';
|
||||
// Log Discord : visiteur bloqué au captcha (possible robot)
|
||||
sendLog("Captcha raté : plus aucun essai restant (possible robot)");
|
||||
} else {
|
||||
$error = 'Ce n’est pas le bon mail. Regarde mieux le message le plus agressif ou trop urgent.';
|
||||
// Log Discord : mauvaise réponse au captcha
|
||||
sendLog("Captcha raté : mauvaise réponse (" . $round['attempts_left'] . " essai(s) restant(s))");
|
||||
}
|
||||
} else {
|
||||
$_SESSION['captcha_valid'] = true;
|
||||
unset($_SESSION['captcha_round']);
|
||||
$success = true;
|
||||
// Log Discord : captcha validé, le visiteur accède au site
|
||||
sendLog("Captcha réussi : nouveau visiteur sur le site");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +338,7 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test anti-robot</title>
|
||||
<link rel="icon" type="image/png" href="./memoji.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
@@ -106,3 +106,39 @@ CREATE TABLE IF NOT EXISTS schools (
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
|
||||
-- markdown : contenu de la page détaillée (LONGTEXT = ~4 Go, taille pratiquement illimitée)
|
||||
-- link_url : lien externe utilisé à la place du markdown si celui-ci est vide
|
||||
-- Les deux sont facultatifs : si les deux sont vides, la carte n'est pas cliquable
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN markdown LONGTEXT NULL,
|
||||
ADD COLUMN link_url VARCHAR(255) NULL;
|
||||
|
||||
ALTER TABLE experiences
|
||||
ADD COLUMN markdown LONGTEXT NULL,
|
||||
ADD COLUMN link_url VARCHAR(255) NULL;
|
||||
|
||||
ALTER TABLE schools
|
||||
ADD COLUMN markdown LONGTEXT NULL,
|
||||
ADD COLUMN link_url VARCHAR(255) NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- Juillet 2026 (3) : PDF en drag & drop pour les projets,
|
||||
-- expériences et écoles, lu depuis un mini lecteur intégré
|
||||
-- ============================================================
|
||||
|
||||
-- pdf_data : contenu binaire du PDF (LONGBLOB = ~4 Go, largement suffisant)
|
||||
-- pdf_filename : nom original du fichier (affiché dans le panel admin et le lecteur)
|
||||
-- Priorité d'affichage sur la page de détails : markdown > pdf > lien externe
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN pdf_data LONGBLOB NULL,
|
||||
ADD COLUMN pdf_filename VARCHAR(255) NULL;
|
||||
|
||||
ALTER TABLE experiences
|
||||
ADD COLUMN pdf_data LONGBLOB NULL,
|
||||
ADD COLUMN pdf_filename VARCHAR(255) NULL;
|
||||
|
||||
ALTER TABLE schools
|
||||
ADD COLUMN pdf_data LONGBLOB NULL,
|
||||
ADD COLUMN pdf_filename VARCHAR(255) NULL;
|
||||
@@ -1,9 +1,30 @@
|
||||
<?php
|
||||
// Ce Footer sera inseré sur des pages grâce a la fonction require_once('./../elements/footer.php')
|
||||
// Rappel des différentes pages : Acceuil, Projets, Contact
|
||||
|
||||
// Notifications "toast" : affichées selon le paramètre présent dans l'URL
|
||||
// (?success=1 après un message de contact, ?login=1 après connexion, ?logout=1 après déconnexion, ?import=1 après un import)
|
||||
$toast = null;
|
||||
if (isset($_GET['success']) && $_GET['success'] === '1') {
|
||||
$toast = 'Votre message a bien été envoyé !';
|
||||
} elseif (isset($_GET['login']) && $_GET['login'] === '1') {
|
||||
$toast = 'Connexion réussie' . (isset($_SESSION['username']) ? ', bonjour ' . htmlspecialchars($_SESSION['username']) . ' !' : ' !');
|
||||
} elseif (isset($_GET['logout']) && $_GET['logout'] === '1') {
|
||||
$toast = 'Vous avez bien été déconnecté.';
|
||||
} elseif (isset($_GET['import']) && $_GET['import'] === '1') {
|
||||
$toast = 'Les données ont bien été importées.';
|
||||
}
|
||||
?>
|
||||
</main>
|
||||
|
||||
<?php if ($toast !== null): ?>
|
||||
<!-- Notification toast : disparaît toute seule, clic pour fermer -->
|
||||
<div class="toast" onclick="this.remove()" role="status">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span><?php echo $toast; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-content">
|
||||
<p class="footer-copyright">
|
||||
|
||||
@@ -14,6 +14,9 @@ require_once('../scripts/requireCaptcha.php');
|
||||
// Ajout des logs sur toutes les pages
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Log de la visite de la page sur Discord (anti-spam : 1 log max par page / minute / visiteur)
|
||||
sendPageLog();
|
||||
|
||||
// Titre de la page : chaque page peut définir $pageTitle avant d'inclure le header
|
||||
$pageTitle = isset($pageTitle) ? $pageTitle . ' — Esteban MOUSSU' : 'Esteban MOUSSU — Portfolio';
|
||||
|
||||
@@ -28,6 +31,9 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo htmlspecialchars($pageTitle); ?></title>
|
||||
|
||||
<!-- Favicon du site -->
|
||||
<link rel="icon" type="image/png" href="../memoji.png">
|
||||
|
||||
<!-- Applique le thème sauvegardé AVANT le chargement du CSS pour éviter un flash sombre/clair -->
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
@@ -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...)
|
||||
@@ -90,6 +96,19 @@ function afficherProjets() {
|
||||
|
||||
// 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>';
|
||||
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
+22
-3
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -109,6 +115,19 @@ function afficherXP() {
|
||||
|
||||
// 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>';
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?php
|
||||
// Ce projet est entièrement développé par Esteban MOUSSU
|
||||
// 2026 - Projet Hébergé par l'entreprise TerraTech Hebergement en collaboration avec l'association Eldoria Network
|
||||
// Le CSS est **probablement** généré par intelligence artificielle ou récupéré d'autre projet, cependant le code PHP / HTML / JS est globalement fait par un humain
|
||||
|
||||
// Le CSS est généré par intelligence artificielle ou récupéré d'autre projet.
|
||||
// Note 1 : Pour lancer un serveur local PHP Depuis VsCode : 'php -v' pour vérifier que php est bien installé et 'php -S localhost:8000' pour lancer le serveur
|
||||
// Note 2 : Je m'aide désormais de l'autocompletion de vsCode pour accélerer le développement avec Copilot (GitHub) ou Devin mais le reste du code est entièrement développé par moi même mais copilot
|
||||
// reste utile pour la génération de commentaires, de code répétitif ou de code basique (exemple : les formulaires de contact, les requêtes SQL basiques etc...)
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+257
-72
@@ -50,18 +50,37 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
// Page du panneau d'administration
|
||||
?>
|
||||
|
||||
<div class="admin-forms">
|
||||
<div class="admin-page">
|
||||
|
||||
<!-- Formulaire de création de projet -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<div class="project-header">
|
||||
<h2><?php echo $projetAModifier ? 'Modifier le projet' : 'Nouveau projet'; ?></h2>
|
||||
<!-- Titre du panneau -->
|
||||
<div class="page-header">
|
||||
<h1>Panneau d'administration</h1>
|
||||
<p>Gère le contenu du site : projets, expériences, écoles, compétences, notification et utilisateurs.</p>
|
||||
</div>
|
||||
|
||||
<!-- Sous-navigation : accès rapide à chaque catégorie -->
|
||||
<nav class="admin-nav">
|
||||
<a href="#admin-projets"><i class="fa-solid fa-folder-open"></i> Projets</a>
|
||||
<a href="#admin-experiences"><i class="fa-solid fa-briefcase"></i> Expériences</a>
|
||||
<a href="#admin-ecoles"><i class="fa-solid fa-graduation-cap"></i> Écoles</a>
|
||||
<a href="#admin-competences"><i class="fa-solid fa-code"></i> Compétences</a>
|
||||
<a href="#admin-notification"><i class="fa-solid fa-bell"></i> Notification</a>
|
||||
<a href="#admin-utilisateurs"><i class="fa-solid fa-users"></i> Utilisateurs</a>
|
||||
<a href="#admin-donnees"><i class="fa-solid fa-database"></i> Données</a>
|
||||
</nav>
|
||||
|
||||
<!-- ========== SECTION PROJETS ========== -->
|
||||
<section id="admin-projets" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-folder-open"></i> Projets</h2>
|
||||
|
||||
<!-- Formulaire (replié par défaut, ouvert automatiquement en mode modification) -->
|
||||
<details class="admin-block" <?php echo $projetAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $projetAModifier ? 'Modifier le projet : ' . htmlspecialchars($projetAModifier['title']) : '+ Nouveau projet'; ?></summary>
|
||||
|
||||
<div class="project-form-card">
|
||||
<!-- enctype multipart/form-data : nécessaire pour l'upload du PDF -->
|
||||
<!-- en envoie les données à addProject.php -->
|
||||
<form method="POST" 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'; ?>">
|
||||
|
||||
<?php if ($projetAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
|
||||
@@ -70,6 +89,9 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<label>Titre *</label>
|
||||
<input type="text" name="title" required value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['title']) : ''; ?>">
|
||||
|
||||
<label>Status (optionnel, affiché sur l'image : En cours, Terminé...)</label>
|
||||
<input type="text" name="status" placeholder="En cours..." value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['status'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Tags (virgules)</label>
|
||||
<input type="text" name="tags" value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['tags']) : ''; ?>">
|
||||
|
||||
@@ -79,22 +101,50 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<label>Lien de l'image *</label>
|
||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['image_url']) : ''; ?>">
|
||||
|
||||
<label>Page détaillée en markdown (optionnel, prioritaire sur le PDF et le lien externe)</label>
|
||||
<textarea name="markdown" class="markdown-textarea" placeholder="# Mon projet Description complète en **markdown**..."><?php echo $projetAModifier ? htmlspecialchars($projetAModifier['markdown'] ?? '') : ''; ?></textarea>
|
||||
|
||||
<label>Document PDF (optionnel, utilisé si pas de markdown)</label>
|
||||
<?php if ($projetAModifier && !empty($projetAModifier['pdf_filename'])): ?>
|
||||
<div class="pdf-current">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span><?php echo htmlspecialchars($projetAModifier['pdf_filename']); ?></span>
|
||||
<a href="../scripts/viewPdf.php?type=projet&id=<?php echo (int) $projetAModifier['id']; ?>" target="_blank" rel="noopener noreferrer">Voir</a>
|
||||
<label class="checkbox-row pdf-remove-row">
|
||||
<input type="checkbox" name="remove_pdf"> Supprimer ce PDF
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="pdf-dropzone" data-input="pdf-project">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<p>Glisse un PDF ici, ou <span class="pdf-dropzone-browse">clique pour choisir un fichier</span></p>
|
||||
<span class="pdf-dropzone-filename"></span>
|
||||
<input type="file" name="pdf" id="pdf-project" accept="application/pdf,.pdf" hidden>
|
||||
</div>
|
||||
|
||||
<label>Lien externe (optionnel, utilisé si pas de markdown ni de PDF)</label>
|
||||
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['link_url'] ?? '') : ''; ?>">
|
||||
|
||||
<input type="submit" name="submitproject" value="<?php echo $projetAModifier ? 'Modifier le projet' : '+ Ajouter le projet'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des projets avec modification / suppression -->
|
||||
<?php afficherProjetsAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION EXPÉRIENCES ========== -->
|
||||
<section id="admin-experiences" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-briefcase"></i> Expériences</h2>
|
||||
|
||||
<details class="admin-block" <?php echo $xpAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $xpAModifier ? 'Modifier l\'expérience : ' . htmlspecialchars($xpAModifier['title']) : '+ Nouvelle expérience'; ?></summary>
|
||||
|
||||
<!-- Formulaire de création d'expérience -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<div class="project-header">
|
||||
<h2><?php echo $xpAModifier ? 'Modifier l\'experience' : 'Nouvelle expérience'; ?></h2>
|
||||
</div>
|
||||
|
||||
<!-- en envoie les données à addXP.php -->
|
||||
<form method="POST" 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'; ?>">
|
||||
|
||||
<?php if ($xpAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
|
||||
@@ -103,8 +153,11 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<label>Titre *</label>
|
||||
<input type="text" name="title" required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['title']) : ''; ?>">
|
||||
|
||||
<label>Status (optionnel, affiché sur l'image : En cours, Terminé...)</label>
|
||||
<input type="text" name="status" placeholder="En cours..." value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['status'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Type *</label>
|
||||
<textarea name="xptype" required><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['xptype']) : ''; ?></textarea>
|
||||
<input type="text" name="xptype" required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['xptype']) : ''; ?>">
|
||||
|
||||
<label>Tags (virgules)</label>
|
||||
<input type="text" name="tags" value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['tags']) : ''; ?>">
|
||||
@@ -113,27 +166,55 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<textarea name="description" required><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['description']) : ''; ?></textarea>
|
||||
|
||||
<label>Durée *</label>
|
||||
<textarea name="duration" required><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['duration']) : ''; ?></textarea>
|
||||
<input type="text" name="duration" required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['duration']) : ''; ?>">
|
||||
|
||||
<label>Lien de l'image *</label>
|
||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['image_url']) : ''; ?>">
|
||||
|
||||
<label>Page détaillée en markdown (optionnel, prioritaire sur le PDF et le lien externe)</label>
|
||||
<textarea name="markdown" class="markdown-textarea" placeholder="# Mon expérience Description complète en **markdown**..."><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['markdown'] ?? '') : ''; ?></textarea>
|
||||
|
||||
<label>Document PDF (optionnel, utilisé si pas de markdown)</label>
|
||||
<?php if ($xpAModifier && !empty($xpAModifier['pdf_filename'])): ?>
|
||||
<div class="pdf-current">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span><?php echo htmlspecialchars($xpAModifier['pdf_filename']); ?></span>
|
||||
<a href="../scripts/viewPdf.php?type=experience&id=<?php echo (int) $xpAModifier['id']; ?>" target="_blank" rel="noopener noreferrer">Voir</a>
|
||||
<label class="checkbox-row pdf-remove-row">
|
||||
<input type="checkbox" name="remove_pdf"> Supprimer ce PDF
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="pdf-dropzone" data-input="pdf-xp">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<p>Glisse un PDF ici, ou <span class="pdf-dropzone-browse">clique pour choisir un fichier</span></p>
|
||||
<span class="pdf-dropzone-filename"></span>
|
||||
<input type="file" name="pdf" id="pdf-xp" accept="application/pdf,.pdf" hidden>
|
||||
</div>
|
||||
|
||||
<label>Lien externe (optionnel, utilisé si pas de markdown ni de PDF)</label>
|
||||
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['link_url'] ?? '') : ''; ?>">
|
||||
|
||||
<input type="submit" name="submitxp" value="<?php echo $xpAModifier ? 'Modifier l\'experience' : '+ Ajouter l\'experience'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des expériences avec modification / suppression -->
|
||||
<?php afficherXPAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION ÉCOLES ========== -->
|
||||
<section id="admin-ecoles" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-graduation-cap"></i> Écoles</h2>
|
||||
|
||||
<details class="admin-block" <?php echo $ecoleAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $ecoleAModifier ? 'Modifier l\'école : ' . htmlspecialchars($ecoleAModifier['title']) : '+ Nouvelle école'; ?></summary>
|
||||
|
||||
<!-- Formulaire de création d'école / formation -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<div class="project-header">
|
||||
<h2><?php echo $ecoleAModifier ? 'Modifier l\'école' : 'Nouvelle école'; ?></h2>
|
||||
</div>
|
||||
|
||||
<!-- on envoie les données à addSchool.php -->
|
||||
<form method="POST" 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'; ?>">
|
||||
|
||||
<?php if ($ecoleAModifier): ?>
|
||||
<input type="hidden" name="id" value="<?php echo (int) $ecoleAModifier['id']; ?>">
|
||||
@@ -142,6 +223,9 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<label>Nom de l'école *</label>
|
||||
<input type="text" name="title" required value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['title']) : ''; ?>">
|
||||
|
||||
<label>Status (optionnel, affiché sur l'image : En cours, Diplômé...)</label>
|
||||
<input type="text" name="status" placeholder="En cours..." value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['status'] ?? '') : ''; ?>">
|
||||
|
||||
<label>Tags (virgules)</label>
|
||||
<input type="text" name="tags" value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['tags']) : ''; ?>">
|
||||
|
||||
@@ -154,20 +238,48 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<label>Lien de l'image *</label>
|
||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['image_url']) : ''; ?>">
|
||||
|
||||
<label>Page détaillée en markdown (optionnel, prioritaire sur le PDF et le lien externe)</label>
|
||||
<textarea name="markdown" class="markdown-textarea" placeholder="# Mon école Description complète en **markdown**..."><?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['markdown'] ?? '') : ''; ?></textarea>
|
||||
|
||||
<label>Document PDF (optionnel, utilisé si pas de markdown)</label>
|
||||
<?php if ($ecoleAModifier && !empty($ecoleAModifier['pdf_filename'])): ?>
|
||||
<div class="pdf-current">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span><?php echo htmlspecialchars($ecoleAModifier['pdf_filename']); ?></span>
|
||||
<a href="../scripts/viewPdf.php?type=ecole&id=<?php echo (int) $ecoleAModifier['id']; ?>" target="_blank" rel="noopener noreferrer">Voir</a>
|
||||
<label class="checkbox-row pdf-remove-row">
|
||||
<input type="checkbox" name="remove_pdf"> Supprimer ce PDF
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="pdf-dropzone" data-input="pdf-school">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<p>Glisse un PDF ici, ou <span class="pdf-dropzone-browse">clique pour choisir un fichier</span></p>
|
||||
<span class="pdf-dropzone-filename"></span>
|
||||
<input type="file" name="pdf" id="pdf-school" accept="application/pdf,.pdf" hidden>
|
||||
</div>
|
||||
|
||||
<label>Lien externe (optionnel, utilisé si pas de markdown ni de PDF)</label>
|
||||
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['link_url'] ?? '') : ''; ?>">
|
||||
|
||||
<input type="submit" name="submitschool" value="<?php echo $ecoleAModifier ? 'Modifier l\'école' : '+ Ajouter l\'école'; ?>" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des écoles avec modification / suppression -->
|
||||
<?php afficherEcolesAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION COMPÉTENCES ========== -->
|
||||
<section id="admin-competences" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-code"></i> Compétences</h2>
|
||||
|
||||
<details class="admin-block" <?php echo $skillAModifier ? 'open' : ''; ?>>
|
||||
<summary><?php echo $skillAModifier ? 'Modifier la catégorie : ' . htmlspecialchars($skillAModifier['title']) : '+ Nouvelle catégorie de compétences'; ?></summary>
|
||||
|
||||
<!-- Formulaire de création de catégorie de compétences -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<div class="project-header">
|
||||
<h2><?php echo $skillAModifier ? 'Modifier la compétence' : 'Nouvelle compétence'; ?></h2>
|
||||
</div>
|
||||
|
||||
<!-- on envoie les données à addSkill.php -->
|
||||
<form method="POST" action="<?php echo $skillAModifier ? '../scripts/updateSkill.php' : '../scripts/addSkill.php'; ?>">
|
||||
|
||||
@@ -191,16 +303,20 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Liste des compétences avec modification / suppression -->
|
||||
<div class="skills-section skills-section-admin">
|
||||
<?php afficherSkillsAdmin(); ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION NOTIFICATION ========== -->
|
||||
<section id="admin-notification" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-bell"></i> Notification de la page d'accueil</h2>
|
||||
|
||||
<!-- Formulaire de la notification de la page d'accueil -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<div class="project-header">
|
||||
<h2>Notification de la page d'accueil</h2>
|
||||
</div>
|
||||
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
|
||||
@@ -221,34 +337,16 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<!-- ========== SECTION UTILISATEURS ========== -->
|
||||
<section id="admin-utilisateurs" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-users"></i> Utilisateurs</h2>
|
||||
|
||||
<?php
|
||||
// Liste des projets / xps => en gros on pourra supprimer, et modifier à la vollée sans aller dans la bdd
|
||||
?>
|
||||
<h2 class="projects-title">Gérer les projets</h2>
|
||||
<?php afficherProjetsAdmin(); ?>
|
||||
<h2 class="projects-title">Gérer les experiences</h2>
|
||||
<?php afficherXPAdmin(); ?>
|
||||
<h2 class="projects-title">Gérer les écoles</h2>
|
||||
<?php afficherEcolesAdmin(); ?>
|
||||
<h2 class="projects-title">Gérer les compétences</h2>
|
||||
<section class="skills-section skills-section-admin">
|
||||
<?php afficherSkillsAdmin(); ?>
|
||||
</section>
|
||||
<?php
|
||||
<details class="admin-block">
|
||||
<summary>+ Nouveau compte</summary>
|
||||
|
||||
// Création de compte
|
||||
?>
|
||||
|
||||
<!-- Formulaire de création de compte -->
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
<div class="project-header">
|
||||
<h2>Nouveau compte</h2>
|
||||
</div>
|
||||
|
||||
<!-- en envoie les données addUser.php -->
|
||||
<form method="POST" action="../scripts/addUser.php">
|
||||
<label>Nom du compte</label>
|
||||
@@ -256,7 +354,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
|
||||
<!-- role admin ou user -->
|
||||
<label>Rôle (admin/user)</label>
|
||||
<textarea name="role" required></textarea>
|
||||
<input type="text" name="role" placeholder="user" required>
|
||||
|
||||
<!-- mot de passe -->
|
||||
<label>Mot de passe</label>
|
||||
@@ -266,14 +364,101 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<input type="submit" name="submituser" value="Ajouter l'utilisateur" class="btn-submit">
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Affichage de la liste des utilisateurs -->
|
||||
<?php afficherUsersAdmin(); ?>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION DONNÉES (EXPORT / IMPORT) ========== -->
|
||||
<section id="admin-donnees" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-database"></i> Données du site</h2>
|
||||
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
|
||||
<!-- Export : télécharge toutes les données en JSON -->
|
||||
<h3 class="admin-data-title"><i class="fa-solid fa-download"></i> Exporter</h3>
|
||||
<p class="admin-data-text">
|
||||
Télécharge une sauvegarde JSON de toutes les données du site
|
||||
(projets, expériences, écoles, compétences, notification, utilisateurs).
|
||||
</p>
|
||||
<a class="btn-submit admin-export-btn" href="../scripts/exportData.php">
|
||||
<i class="fa-solid fa-download"></i> Exporter les données (JSON)
|
||||
</a>
|
||||
|
||||
<hr class="admin-data-separator">
|
||||
|
||||
<!-- Import : écrase les données actuelles avec celles du fichier -->
|
||||
<h3 class="admin-data-title"><i class="fa-solid fa-upload"></i> Importer</h3>
|
||||
<p class="admin-data-text admin-data-warning">
|
||||
⚠️ L'import <strong>écrase définitivement</strong> les données actuelles des tables
|
||||
présentes dans le fichier. Fais un export avant, au cas où !
|
||||
</p>
|
||||
<form method="POST" action="../scripts/importData.php" enctype="multipart/form-data"
|
||||
onsubmit="return confirm('ATTENTION : les données actuelles du site vont être écrasées par celles du fichier. Continuer ?');">
|
||||
<input type="file" name="jsonfile" accept=".json,application/json" required>
|
||||
<input type="submit" value="Importer les données" class="btn-submit btn-delete">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Liste des utilisateurs -->
|
||||
<h2 class="projects-title">Gérer les utilisateurs</h2>
|
||||
<!-- Gestion des zones de drag & drop PDF (projets, expériences, écoles) -->
|
||||
<script>
|
||||
(function () {
|
||||
document.querySelectorAll('.pdf-dropzone').forEach(function (zone) {
|
||||
var input = document.getElementById(zone.dataset.input);
|
||||
var filenameEl = zone.querySelector('.pdf-dropzone-filename');
|
||||
|
||||
function afficherFichier(file) {
|
||||
if (!file) return;
|
||||
if (file.type !== 'application/pdf') {
|
||||
filenameEl.textContent = 'Ce fichier n\'est pas un PDF.';
|
||||
zone.classList.remove('has-file');
|
||||
return;
|
||||
}
|
||||
filenameEl.textContent = file.name;
|
||||
zone.classList.add('has-file');
|
||||
}
|
||||
|
||||
// Clic sur la zone = ouvre le sélecteur de fichier
|
||||
zone.addEventListener('click', function () {
|
||||
input.click();
|
||||
});
|
||||
|
||||
input.addEventListener('change', function () {
|
||||
afficherFichier(input.files[0]);
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach(function (evt) {
|
||||
zone.addEventListener(evt, function (e) {
|
||||
e.preventDefault();
|
||||
zone.classList.add('dragging');
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(function (evt) {
|
||||
zone.addEventListener(evt, function (e) {
|
||||
e.preventDefault();
|
||||
zone.classList.remove('dragging');
|
||||
});
|
||||
});
|
||||
|
||||
zone.addEventListener('drop', function (e) {
|
||||
var file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
input.files = e.dataTransfer.files;
|
||||
afficherFichier(file);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Affichage de la liste des utilisateurs -->
|
||||
<?php
|
||||
afficherUsersAdmin();
|
||||
|
||||
// On ajoute ici le footer
|
||||
require_once('../elements/footer.php'); ?>
|
||||
@@ -0,0 +1,159 @@
|
||||
<?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'); ?>
|
||||
+7
-2
@@ -28,14 +28,19 @@ afficherNotice();
|
||||
<a href="../pages/projects.php" class="btn btn-primary"><i class="fa-solid fa-folder-open"></i> Voir mes projets</a>
|
||||
<a href="../pages/contact.php" class="btn btn-ghost"><i class="fa-solid fa-paper-plane"></i> Me contacter</a>
|
||||
</div>
|
||||
|
||||
<!-- Flèche animée qui invite à défiler vers le bas -->
|
||||
<a href="#explorer" class="scroll-indicator" aria-label="Défiler vers le bas">
|
||||
<i class="fa-solid fa-chevron-down"></i>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<!-- Section d'exploration rapide -->
|
||||
<section class="quick-links-section">
|
||||
<section class="quick-links-section" id="explorer">
|
||||
<div class="quick-links-header">
|
||||
<!-- Titre et description de la section -->
|
||||
<h2>Explorer</h2>
|
||||
<p>Accède rapidement aux sections principales du portfolio.</p>
|
||||
<p>Accède rapidement aux sections principales de mon portfolio.</p>
|
||||
</div>
|
||||
<!-- Grille de cartes de redirection vers les différentes page -->
|
||||
<div class="quick-links-grid">
|
||||
|
||||
+43
-2
@@ -16,7 +16,7 @@ if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
||||
// Affichages des xp pro
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header" id="experiences">
|
||||
<h1>Expériences professionnelles</h1>
|
||||
<p>Mes stages et missions réalisées en entreprise.</p>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@ require_once('../functions/listSchool.php');
|
||||
if (!empty(listeEcoles())) {
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header" id="ecoles">
|
||||
<h2>Mes écoles</h2>
|
||||
<p>Mon parcours scolaire et mes formations.</p>
|
||||
</div>
|
||||
@@ -42,6 +42,47 @@ if (!empty(listeEcoles())) {
|
||||
<?php
|
||||
// Appel de ma fonction dans Functions/listSchool.php
|
||||
afficherEcoles();
|
||||
?>
|
||||
|
||||
<!-- Bouton flottant : icône chapeau de diplômé = descendre vers les écoles,
|
||||
icône sac de travail = remonter vers les expériences -->
|
||||
<button class="section-jump" id="section-jump" aria-label="Basculer entre expériences et écoles">
|
||||
<i class="fa-solid fa-graduation-cap"></i>
|
||||
<i class="fa-solid fa-briefcase"></i>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var btn = document.getElementById('section-jump');
|
||||
var ecoles = document.getElementById('ecoles');
|
||||
if (!btn || !ecoles) return;
|
||||
|
||||
// Vrai quand la moitié de l'écran a dépassé le début de la section écoles
|
||||
function estDansEcoles() {
|
||||
return window.scrollY + window.innerHeight / 2 >= ecoles.offsetTop;
|
||||
}
|
||||
|
||||
// Change l'icône du bouton selon la section où on se trouve
|
||||
function majIcone() {
|
||||
btn.classList.toggle('mode-ecoles', estDansEcoles());
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (estDansEcoles()) {
|
||||
// On est dans les écoles : remonter aux expériences
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
// On est dans les expériences : descendre aux écoles
|
||||
ecoles.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', majIcone);
|
||||
majIcone();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
// Ajout du footer
|
||||
|
||||
+35
-7
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -8,28 +10,54 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Récupération des données dans la requete POST
|
||||
$title = $_POST['title'] ?? '';
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$tags = $_POST['tags'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$image_url = $_POST['imageurl'] ?? '';
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
// Le fichier envoyé n'est pas valide : on affiche l'erreur et on s'arrête
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
$pdfData = is_array($pdfResultat) ? $pdfResultat['data'] : null;
|
||||
$pdfFilename = is_array($pdfResultat) ? $pdfResultat['filename'] : null;
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vide
|
||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer le nouveau projet
|
||||
$stmt = $dbh->prepare('INSERT INTO projects (title, tags, description, image_url) VALUES (:title, :tags, :description, :image_url)');
|
||||
$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)');
|
||||
|
||||
// On execute la requete préparée
|
||||
$stmt->execute([
|
||||
':title' => $title,
|
||||
':tags' => $tags,
|
||||
':description' => $description,
|
||||
':image_url' => $image_url
|
||||
]);
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout du projet : " . $title);
|
||||
|
||||
// Redirection vers projects.php ds que la requete est executée
|
||||
header('Location: ../pages/projects.php');
|
||||
|
||||
+35
-8
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -8,30 +10,55 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Récupération des données dans la requete POST
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$duration = trim($_POST['duration'] ?? '');
|
||||
$image_url = trim($_POST['imageurl'] ?? '');
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
$pdfData = is_array($pdfResultat) ? $pdfResultat['data'] : null;
|
||||
$pdfFilename = is_array($pdfResultat) ? $pdfResultat['filename'] : null;
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vide
|
||||
if (!empty($title) && !empty($description) && !empty($duration) && !empty($image_url)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer la nouvelle école
|
||||
$stmt = $dbh->prepare('INSERT INTO schools (title, tags, description, duration, image_url) VALUES (:title, :tags, :description, :duration, :image_url)');
|
||||
$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)');
|
||||
|
||||
// On execute la requete préparée
|
||||
$stmt->execute([
|
||||
':title' => $title,
|
||||
':tags' => $tags,
|
||||
':description' => $description,
|
||||
':duration' => $duration,
|
||||
':image_url' => $image_url
|
||||
]);
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout de l'école : " . $title);
|
||||
|
||||
// Redirection vers xp.php dès que la requete est executée
|
||||
header('Location: ../pages/xp.php');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -31,6 +32,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
':position' => $position
|
||||
]);
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout de la catégorie de compétences : " . $title);
|
||||
|
||||
// Redirection vers skills.php dès que la requete est executée
|
||||
header('Location: ../pages/skills.php');
|
||||
exit;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
session_start();
|
||||
// Ajout de connexion à la BDD
|
||||
require_once("../functions/databaseConnection.php");
|
||||
// Ajout des logs Discord
|
||||
require_once("../functions/sendLogs.php");
|
||||
|
||||
// Vérification que l'utilisateur est bien admin
|
||||
if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||
@@ -58,6 +60,9 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
":created_at" => $dateCreation
|
||||
]);
|
||||
|
||||
// Log Discord
|
||||
sendLog("Création du compte : " . $user . " (rôle : " . $role . ")");
|
||||
|
||||
// Redirection vers la page admin après succès
|
||||
header("Location: ../pages/admin.php");
|
||||
exit();
|
||||
|
||||
+36
-9
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once '../functions/databaseConnection.php';
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -8,32 +10,57 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite pour éviter un échec silencieux
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Récupération des données dans la requete POST
|
||||
$title = $_POST['title'] ?? '';
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$xptype = $_POST['xptype'] ?? '';
|
||||
$tags = $_POST['tags'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$duration = $_POST['duration'] ?? '';
|
||||
$image_url = $_POST['imageurl'] ?? '';
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
$pdfData = is_array($pdfResultat) ? $pdfResultat['data'] : null;
|
||||
$pdfFilename = is_array($pdfResultat) ? $pdfResultat['filename'] : null;
|
||||
|
||||
// On vérifie que les champs obligatoires ne sont pas vide
|
||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||
try {
|
||||
// On prépare la requête SQL pour insérer la nouvelle experience
|
||||
$stmt = $dbh->prepare('INSERT INTO experiences (title, xptype, tags, description, duration, image_url) VALUES (:title, :xptype, :tags, :description, :duration, :image_url)');
|
||||
$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)');
|
||||
|
||||
// On execute la requete préparée
|
||||
$stmt->execute([
|
||||
':title' => $title,
|
||||
':xptype' => $xptype,
|
||||
':tags' => $tags,
|
||||
':description' => $description,
|
||||
':duration' => $duration,
|
||||
':image_url' => $image_url
|
||||
]);
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $image_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
// Log Discord
|
||||
sendLog("Ajout de l'expérience : " . $title);
|
||||
|
||||
// Redirection vers xp.php ds que la requete est executée
|
||||
header('Location: ../pages/xp.php');
|
||||
|
||||
+10
-1
@@ -8,6 +8,9 @@ session_start();
|
||||
// Ajout de connexion a la bdd
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Ajout des logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// La requête doit être un POST sinon on l'a refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ./../pages/login.php');
|
||||
@@ -29,6 +32,8 @@ try {
|
||||
|
||||
// On regarde si le mdp correspond à celui en bdd
|
||||
if (!$user || !password_verify($password, $user['password'])) {
|
||||
// Log Discord : tentative de connexion échouée (utile pour repérer le bruteforce)
|
||||
sendLog("Tentative de connexion échouée avec l'identifiant : " . $username);
|
||||
header('Location: ./../pages/login.php');
|
||||
exit;
|
||||
}
|
||||
@@ -46,7 +51,11 @@ try {
|
||||
error_log('Le serveur WEB ne peut pas modifier la date de dernière connexion : ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord : connexion réussie
|
||||
sendLog("Connexion réussie", $user['username']);
|
||||
|
||||
//Redirection vers home.php puisque que l'utilisateur est forcément connecté
|
||||
header('Location: ./../pages/home.php');
|
||||
// (?login=1 déclenche la notification "Connexion réussie" sur la page)
|
||||
header('Location: ./../pages/home.php?login=1');
|
||||
exit;
|
||||
?>
|
||||
@@ -5,6 +5,12 @@
|
||||
// Démarrage de session
|
||||
session_start();
|
||||
|
||||
// Ajout des logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Log Discord AVANT la destruction de session (sinon on perd le nom d'utilisateur)
|
||||
sendLog("Déconnexion");
|
||||
|
||||
// On sauvegarde l'état du captcha avant destruction
|
||||
$captchaAlreadyValid = isset($_SESSION['captcha_valid']) && $_SESSION['captcha_valid'] === true;
|
||||
|
||||
@@ -20,5 +26,6 @@ if ($captchaAlreadyValid) {
|
||||
}
|
||||
|
||||
// Redirection de l'utilisateur vers l'accueil
|
||||
header('Location: ../index.php');
|
||||
// (?logout=1 déclenche la notification "Vous avez bien été déconnecté" sur la page)
|
||||
header('Location: ../pages/home.php?logout=1');
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
// Export de toutes les données du site en JSON (comme un dbdump mais en JSON)
|
||||
// Le fichier téléchargé peut ensuite être ré-importé via importData.php
|
||||
|
||||
// On démarre la session
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Liste blanche des tables et colonnes exportées (empêche toute injection via noms de colonnes)
|
||||
$tables = [
|
||||
'settings' => ['name', 'value'],
|
||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||
];
|
||||
|
||||
// Colonnes binaires : le JSON ne peut pas contenir de binaire brut, on les encode en base64
|
||||
// (+33% de taille mais nécessaire pour que le PDF survive à un export/import complet)
|
||||
$colonnesBinaires = [
|
||||
'projects' => ['pdf_data'],
|
||||
'experiences' => ['pdf_data'],
|
||||
'schools' => ['pdf_data'],
|
||||
];
|
||||
|
||||
// Métadonnées de l'export (pratique pour s'y retrouver dans les sauvegardes)
|
||||
$export = [
|
||||
'_meta' => [
|
||||
'site' => 'Portfolio Esteban MOUSSU',
|
||||
'date_export' => date('Y-m-d H:i:s'),
|
||||
'version' => 1
|
||||
]
|
||||
];
|
||||
|
||||
// Pour chaque table on récupère toutes les lignes
|
||||
foreach ($tables as $table => $colonnes) {
|
||||
try {
|
||||
// SELECT * puis filtrage par liste blanche : fonctionne même si certaines
|
||||
// colonnes optionnelles (markdown, link_url, pdf_data...) n'existent pas encore
|
||||
$stmt = $dbh->query('SELECT * FROM `' . $table . '`');
|
||||
$lignes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$binaires = $colonnesBinaires[$table] ?? [];
|
||||
|
||||
// On ne garde que les colonnes de la liste blanche, et on encode les colonnes binaires
|
||||
$export[$table] = array_map(function ($ligne) use ($colonnes, $binaires) {
|
||||
$ligne = array_intersect_key($ligne, array_flip($colonnes));
|
||||
foreach ($binaires as $colonne) {
|
||||
if (isset($ligne[$colonne]) && $ligne[$colonne] !== null) {
|
||||
$ligne[$colonne] = base64_encode($ligne[$colonne]);
|
||||
}
|
||||
}
|
||||
return $ligne;
|
||||
}, $lignes);
|
||||
} catch (PDOException $e) {
|
||||
// Si une table n'existe pas encore on l'exporte vide plutôt que de bloquer l'export
|
||||
$export[$table] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Export des données du site en JSON");
|
||||
|
||||
// Envoi du fichier JSON en téléchargement
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="portfolio-backup-' . date('Y-m-d-His') . '.json"');
|
||||
echo json_encode($export, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
// Import d'une sauvegarde JSON (créée par exportData.php)
|
||||
// ATTENTION : les données actuelles des tables présentes dans le fichier sont ÉCRASÉES
|
||||
|
||||
// On démarre la session
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../pages/home.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// La requête doit être un POST avec un fichier sinon on refuse
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['jsonfile']) || $_FILES['jsonfile']['error'] !== UPLOAD_ERR_OK) {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Lecture et décodage du fichier JSON
|
||||
$contenu = file_get_contents($_FILES['jsonfile']['tmp_name']);
|
||||
$donnees = json_decode($contenu, true);
|
||||
|
||||
if (!is_array($donnees)) {
|
||||
die("Erreur : le fichier n'est pas un JSON valide.");
|
||||
}
|
||||
|
||||
// Même liste blanche que l'export : seules ces tables / colonnes peuvent être importées
|
||||
$tables = [
|
||||
'settings' => ['name', 'value'],
|
||||
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'pdf_data', 'pdf_filename', 'created_at'],
|
||||
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||
];
|
||||
|
||||
// Colonnes binaires : encodées en base64 par exportData.php, à redécoder ici
|
||||
$colonnesBinaires = [
|
||||
'projects' => ['pdf_data'],
|
||||
'experiences' => ['pdf_data'],
|
||||
'schools' => ['pdf_data'],
|
||||
];
|
||||
|
||||
try {
|
||||
// Transaction : si une erreur survient, TOUT est annulé (la base reste intacte)
|
||||
$dbh->beginTransaction();
|
||||
|
||||
foreach ($tables as $table => $colonnesAutorisees) {
|
||||
// On ne touche qu'aux tables présentes dans le fichier
|
||||
if (!isset($donnees[$table]) || !is_array($donnees[$table])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sécurité : on refuse de vider la table users si la sauvegarde n'en contient aucun
|
||||
// (sinon l'admin se retrouverait enfermé dehors sans aucun compte)
|
||||
if ($table === 'users' && count($donnees[$table]) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// On écrase les données existantes de la table
|
||||
$dbh->exec('DELETE FROM `' . $table . '`');
|
||||
|
||||
// Colonnes binaires (PDF) de cette table, à redécoder du base64
|
||||
$binaires = $colonnesBinaires[$table] ?? [];
|
||||
|
||||
// Puis on insère chaque ligne de la sauvegarde
|
||||
foreach ($donnees[$table] as $ligne) {
|
||||
if (!is_array($ligne)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// On ne garde que les colonnes autorisées ET présentes dans la ligne
|
||||
$colonnes = array_values(array_intersect($colonnesAutorisees, array_keys($ligne)));
|
||||
if (empty($colonnes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$placeholders = array_map(function ($c) { return ':' . $c; }, $colonnes);
|
||||
$stmt = $dbh->prepare(
|
||||
'INSERT INTO `' . $table . '` (`' . implode('`, `', $colonnes) . '`) VALUES (' . implode(', ', $placeholders) . ')'
|
||||
);
|
||||
|
||||
foreach ($colonnes as $colonne) {
|
||||
$valeur = $ligne[$colonne];
|
||||
|
||||
// Les colonnes binaires (pdf_data) sont ré-encodées en base64 par l'export : on les redécode
|
||||
if (in_array($colonne, $binaires, true) && $valeur !== null) {
|
||||
$stmt->bindValue(':' . $colonne, base64_decode($valeur), PDO::PARAM_LOB);
|
||||
} else {
|
||||
$stmt->bindValue(':' . $colonne, $valeur);
|
||||
}
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
||||
|
||||
// Tout s'est bien passé : on valide définitivement
|
||||
$dbh->commit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
// En cas d'erreur on annule tout : la base revient à son état d'avant l'import
|
||||
$dbh->rollBack();
|
||||
die("Erreur pendant l'import, aucune donnée n'a été modifiée : " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Import d'une sauvegarde JSON : données du site écrasées");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php (?import=1 déclenche la notification de succès)
|
||||
header('Location: ../pages/admin.php?import=1');
|
||||
exit;
|
||||
@@ -3,6 +3,8 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -26,6 +28,9 @@ try { // On essaie d'executer une requête qui est préparée
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression du projet :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression du projet id " . $id);
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -3,6 +3,8 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -26,6 +28,10 @@ try { // On essaie d'executer une requête qui est préparée
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression de l'école :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression de l'école id " . $id);
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -26,6 +28,10 @@ try { // On essaie d'executer une requête qui est préparée
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression de la compétence :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression de la catégorie de compétences id " . $id);
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
session_start();
|
||||
// Ajout de connexion à la BDD
|
||||
require_once("../functions/databaseConnection.php");
|
||||
// Ajout des logs Discord
|
||||
require_once("../functions/sendLogs.php");
|
||||
|
||||
// Vérification que l'utilisateur est bien admin
|
||||
if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||
@@ -26,6 +28,9 @@ if ($id == "") {
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression du compte id " . $id);
|
||||
|
||||
// Redirection vers la page admin après suppression
|
||||
header("Location: ../pages/admin.php");
|
||||
exit();
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -26,6 +28,10 @@ try { // On essaie d'executer une requête qui est préparée
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la suppression de l experience :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Suppression de l'expérience id " . $id);
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -2,6 +2,10 @@
|
||||
// Envoi de message de contact via WebHook discord => Correction de faille de sécurité en cachant le webhook dans un fichier à part
|
||||
require_once('../../idDiscord.php');
|
||||
|
||||
// Ajout des logs Discord
|
||||
session_start();
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Envoi de formulaire de contact via WebHook discord
|
||||
$webhook = ID_DISCORD2; // URL du webhook Discord
|
||||
// EXEMPLE de a quoi ressemble idDiscord :
|
||||
@@ -55,12 +59,13 @@ if ($subject == "" || $name == "" || $mail == "" || $message == "") {
|
||||
$resultat = curl_exec($ch);
|
||||
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
// (pas de curl_close : déprécié depuis PHP 8.5, la ressource se libère toute seule)
|
||||
if ($codeHttp == 204) { // Si ça renvoi 204 alors le message a été correctement envoyé
|
||||
curl_close($ch); // Fermer la ressource curl lancée
|
||||
// Log Discord : message de contact envoyé
|
||||
sendLog("Nouveau message de contact envoyé par : " . $name);
|
||||
header("Location: ../pages/contact.php?success=1");
|
||||
exit();
|
||||
} else { // Sinon c'est que ça n'a pas été envoyé (discord down / webhook expiré / etcc...)
|
||||
curl_close($ch); // Fermer la ressource curl lancée
|
||||
echo "Le message n'a pas pu être envoyé sur Discord.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -10,6 +13,12 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
empty($_POST['title']) ||
|
||||
@@ -23,25 +32,64 @@ if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
// On récupère les données pour les mettres en variables locales
|
||||
$id = (int) $_POST['id'];
|
||||
$title = trim($_POST['title']);
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$description = trim($_POST['description']);
|
||||
$imageurl = trim($_POST['imageurl']);
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
// Le fichier envoyé n'est pas valide : on affiche l'erreur et on s'arrête
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le PDF n'est mis à jour que si un nouveau fichier est envoyé OU si la case
|
||||
// "Supprimer ce PDF" est cochée. Sinon on garde le PDF actuel intact.
|
||||
$updatePdf = false;
|
||||
$pdfData = null;
|
||||
$pdfFilename = null;
|
||||
if (is_array($pdfResultat)) {
|
||||
$updatePdf = true;
|
||||
$pdfData = $pdfResultat['data'];
|
||||
$pdfFilename = $pdfResultat['filename'];
|
||||
} elseif (isset($_POST['remove_pdf'])) {
|
||||
$updatePdf = true;
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('
|
||||
$sql = '
|
||||
UPDATE projects
|
||||
SET title = :title, tags = :tags, description = :description, image_url = :image_url
|
||||
WHERE id = :id
|
||||
');
|
||||
SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url';
|
||||
if ($updatePdf) {
|
||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
||||
}
|
||||
$sql .= ' WHERE id = :id';
|
||||
|
||||
$stmt = $dbh->prepare($sql);
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
if ($updatePdf) {
|
||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification du projet :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification du projet : " . $title . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -3,6 +3,9 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -10,6 +13,12 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
empty($_POST['title']) ||
|
||||
@@ -24,27 +33,65 @@ if ( // Si les champs sont vides on renvoie vers admin.php
|
||||
// On récupère les données pour les mettres en variables locales
|
||||
$id = (int) $_POST['id'];
|
||||
$title = trim($_POST['title']);
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$description = trim($_POST['description']);
|
||||
$duration = trim($_POST['duration']);
|
||||
$imageurl = trim($_POST['imageurl']);
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le PDF n'est mis à jour que si un nouveau fichier est envoyé OU si la case
|
||||
// "Supprimer ce PDF" est cochée. Sinon on garde le PDF actuel intact.
|
||||
$updatePdf = false;
|
||||
$pdfData = null;
|
||||
$pdfFilename = null;
|
||||
if (is_array($pdfResultat)) {
|
||||
$updatePdf = true;
|
||||
$pdfData = $pdfResultat['data'];
|
||||
$pdfFilename = $pdfResultat['filename'];
|
||||
} elseif (isset($_POST['remove_pdf'])) {
|
||||
$updatePdf = true;
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('
|
||||
$sql = '
|
||||
UPDATE schools
|
||||
SET title = :title, tags = :tags, description = :description, duration = :duration, image_url = :image_url
|
||||
WHERE id = :id
|
||||
');
|
||||
SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url';
|
||||
if ($updatePdf) {
|
||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
||||
}
|
||||
$sql .= ' WHERE id = :id';
|
||||
|
||||
$stmt = $dbh->prepare($sql);
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
if ($updatePdf) {
|
||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de l'école :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification de l'école : " . $title . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -38,6 +40,9 @@ try { // On essaie d'executer une requête qui est préparée pour chaque régla
|
||||
die("Erreur lors de la modification de la notification :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification de la notification de la page d'accueil");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -41,6 +43,10 @@ try { // On essaie d'executer une requête qui est préparée
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de la compétence :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification de la catégorie de compétences : " . $title . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
|
||||
+51
-4
@@ -3,6 +3,9 @@
|
||||
session_start();
|
||||
// On ajoute la DB
|
||||
require_once('../functions/databaseConnection.php');
|
||||
// On ajoute les logs Discord
|
||||
require_once('../functions/sendLogs.php');
|
||||
require_once('../functions/pdfUpload.php');
|
||||
|
||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
@@ -10,6 +13,12 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||
if (postDepasseLimite()) {
|
||||
die(messageDepassementPost());
|
||||
}
|
||||
|
||||
// Si le formulaire à un champ vide
|
||||
if (
|
||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||
@@ -26,29 +35,67 @@ if (
|
||||
//On récupère les données pour les mettres en variables locales
|
||||
$id = (int) $_POST['id'];
|
||||
$title = trim($_POST['title']);
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$xptype = trim($_POST['xptype']);
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$description = trim($_POST['description']);
|
||||
$duration = trim($_POST['duration']);
|
||||
$imageurl = trim($_POST['imageurl']);
|
||||
$markdown = trim($_POST['markdown'] ?? '');
|
||||
$link_url = trim($_POST['linkurl'] ?? '');
|
||||
|
||||
// Lecture et validation du PDF envoyé (optionnel)
|
||||
$pdfResultat = lireUploadPdf('pdf');
|
||||
if (is_string($pdfResultat)) {
|
||||
echo $pdfResultat;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Le PDF n'est mis à jour que si un nouveau fichier est envoyé OU si la case
|
||||
// "Supprimer ce PDF" est cochée. Sinon on garde le PDF actuel intact.
|
||||
$updatePdf = false;
|
||||
$pdfData = null;
|
||||
$pdfFilename = null;
|
||||
if (is_array($pdfResultat)) {
|
||||
$updatePdf = true;
|
||||
$pdfData = $pdfResultat['data'];
|
||||
$pdfFilename = $pdfResultat['filename'];
|
||||
} elseif (isset($_POST['remove_pdf'])) {
|
||||
$updatePdf = true;
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée
|
||||
$stmt = $dbh->prepare('
|
||||
$sql = '
|
||||
UPDATE experiences
|
||||
SET title = :title, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url
|
||||
WHERE id = :id
|
||||
');
|
||||
SET title = :title, status = :status, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url';
|
||||
if ($updatePdf) {
|
||||
$sql .= ', pdf_data = :pdf_data, pdf_filename = :pdf_filename';
|
||||
}
|
||||
$sql .= ' WHERE id = :id';
|
||||
|
||||
$stmt = $dbh->prepare($sql);
|
||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||
if ($updatePdf) {
|
||||
$stmt->bindValue(':pdf_data', $pdfData, $pdfData === null ? PDO::PARAM_NULL : PDO::PARAM_LOB);
|
||||
$stmt->bindValue(':pdf_filename', $pdfFilename, $pdfFilename === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||
} catch (PDOException $e) {
|
||||
die("Erreur lors de la modification de l experience :" . $e->getMessage());
|
||||
}
|
||||
|
||||
// Log Discord
|
||||
sendLog("Modification de l'expérience : " . $title . " (id " . $id . ")");
|
||||
|
||||
// On renvoie l'utilisateur sur admin.php
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
// Sert le PDF stocké en base pour un projet / une expérience / une école
|
||||
// Utilisé en <iframe> par la page de détails (mini lecteur) et pour le téléchargement direct
|
||||
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
// Liste blanche des types autorisés : empêche toute injection de nom de table
|
||||
$tables = [
|
||||
'projet' => 'projects',
|
||||
'experience' => 'experiences',
|
||||
'ecole' => 'schools',
|
||||
];
|
||||
|
||||
$type = $_GET['type'] ?? '';
|
||||
$id = $_GET['id'] ?? '';
|
||||
|
||||
if (!isset($tables[$type]) || !is_numeric($id)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('SELECT pdf_data, pdf_filename FROM `' . $tables[$type] . '` WHERE id = :id');
|
||||
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$row || $row['pdf_data'] === null) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$filename = !empty($row['pdf_filename']) ? $row['pdf_filename'] : 'document.pdf';
|
||||
|
||||
// ?download=1 force le téléchargement, sinon le PDF s'affiche dans le mini lecteur (iframe)
|
||||
$disposition = (isset($_GET['download']) && $_GET['download'] === '1') ? 'attachment' : 'inline';
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: ' . $disposition . '; filename="' . basename($filename) . '"');
|
||||
header('Content-Length: ' . strlen($row['pdf_data']));
|
||||
header('Cache-Control: private, max-age=3600');
|
||||
echo $row['pdf_data'];
|
||||
exit;
|
||||
+648
-1
@@ -357,10 +357,58 @@ img {
|
||||
.hero {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: clamp(56px, 10vw, 120px) clamp(20px, 4vw, 40px) clamp(30px, 5vw, 60px);
|
||||
/* Le hero occupe presque tout l'écran pour un rendu plus aéré */
|
||||
min-height: calc(100vh - 220px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: clamp(70px, 12vw, 140px) clamp(20px, 4vw, 40px) clamp(90px, 12vw, 130px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Flèche animée qui invite à défiler vers le bas */
|
||||
.scroll-indicator {
|
||||
position: absolute;
|
||||
bottom: 26px;
|
||||
left: 50%;
|
||||
margin-left: -23px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
color: var(--accent-light);
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid var(--accent-border);
|
||||
font-size: 17px;
|
||||
opacity: 0;
|
||||
animation: scrollFadeIn 0.6s ease 0.9s forwards, scrollBounce 2s ease-in-out 1.5s infinite;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.scroll-indicator:hover {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@keyframes scrollFadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scrollBounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(9px);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1415,6 +1463,596 @@ img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==== CARTES CLIQUABLES (page markdown / lien externe) ==== */
|
||||
|
||||
/* Indicateur en bas de carte ; le ::after rend TOUTE la carte cliquable quand c'est un lien */
|
||||
.projects-list .project-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
a.card-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.card-link-none {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ==== PAGE DE DÉTAILS (markdown) ==== */
|
||||
|
||||
.details-page {
|
||||
max-width: 920px;
|
||||
margin: clamp(24px, 4vw, 40px) auto clamp(48px, 7vw, 72px);
|
||||
padding: 0 clamp(14px, 4vw, 24px);
|
||||
}
|
||||
|
||||
.details-back {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.details-card {
|
||||
background: linear-gradient(180deg, var(--surface) 0%, var(--bg-soft) 100%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.details-banner img {
|
||||
width: 100%;
|
||||
height: clamp(200px, 32vw, 320px);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.details-content {
|
||||
padding: clamp(22px, 4vw, 40px);
|
||||
}
|
||||
|
||||
.details-type {
|
||||
display: inline-block;
|
||||
padding: 6px 14px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid var(--accent-border);
|
||||
color: var(--accent-light);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.details-content h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: clamp(26px, 4vw, 38px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.details-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0 16px;
|
||||
color: var(--text-muted);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.details-meta i {
|
||||
color: var(--accent-light);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Contenu markdown rendu (via marked.js) */
|
||||
.markdown-content {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-soft);
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3,
|
||||
.markdown-content h4 {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 28px 0 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.markdown-content h1 { font-size: 28px; }
|
||||
.markdown-content h2 { font-size: 23px; }
|
||||
.markdown-content h3 { font-size: 19px; }
|
||||
.markdown-content h4 { font-size: 17px; }
|
||||
|
||||
.markdown-content p {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: var(--accent-light);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
margin: 0 0 14px;
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
border-radius: var(--radius-sm);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 2px 7px;
|
||||
font-size: 14px;
|
||||
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
margin: 0 0 16px;
|
||||
padding: 10px 18px;
|
||||
border-left: 3px solid var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
margin: 0 0 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-content th,
|
||||
.markdown-content td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content th {
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ==== MINI LECTEUR PDF (page de détails) ==== */
|
||||
|
||||
.pdf-viewer-wrap {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pdf-viewer-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-soft);
|
||||
font-size: 14.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pdf-viewer-toolbar i {
|
||||
color: var(--accent-light);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pdf-download-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 13.5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pdf-viewer {
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
min-height: 420px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #525659;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.pdf-viewer {
|
||||
height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==== PANEL ADMIN (sous-catégories) ==== */
|
||||
|
||||
.admin-page {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Sous-navigation par catégorie */
|
||||
.admin-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-width: 1200px;
|
||||
margin: 20px auto 8px;
|
||||
padding: 0 clamp(16px, 4vw, 40px);
|
||||
}
|
||||
|
||||
.admin-nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 16px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-soft);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.admin-nav a:hover {
|
||||
color: var(--accent-light);
|
||||
border-color: var(--accent-border);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.admin-nav a i {
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
/* Sections du panel */
|
||||
.admin-section {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 26px 0 6px;
|
||||
/* L'ancre s'arrête sous le header sticky */
|
||||
scroll-margin-top: 84px;
|
||||
}
|
||||
|
||||
.admin-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 16px;
|
||||
padding: 0 clamp(16px, 4vw, 40px);
|
||||
font-size: clamp(21px, 3vw, 26px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.admin-section-title i {
|
||||
color: var(--accent-light);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Bloc repliable contenant chaque formulaire */
|
||||
.admin-block {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 20px;
|
||||
padding: 0 clamp(16px, 4vw, 40px);
|
||||
}
|
||||
|
||||
.admin-block summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 11px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.admin-block summary:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
|
||||
.admin-block[open] summary {
|
||||
border-color: var(--accent-border);
|
||||
color: var(--accent-light);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.admin-block .project-form-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Grand champ markdown dans les formulaires admin */
|
||||
.project-form-card .markdown-textarea {
|
||||
min-height: 220px;
|
||||
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
/* ==== DRAG & DROP PDF (panel admin) ==== */
|
||||
|
||||
/* Bandeau indiquant le PDF déjà attaché, avec option de suppression */
|
||||
.pdf-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-soft);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pdf-current i {
|
||||
color: var(--accent-light);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.pdf-current a {
|
||||
color: var(--accent-light);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pdf-current a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pdf-remove-row {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
|
||||
/* Zone de dépôt du fichier PDF */
|
||||
.pdf-dropzone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 28px 20px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
background: var(--bg-soft);
|
||||
border: 2px dashed var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.pdf-dropzone i {
|
||||
font-size: 26px;
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.pdf-dropzone p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pdf-dropzone-browse {
|
||||
color: var(--accent-light);
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pdf-dropzone-filename {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Survol pendant qu'un fichier est glissé au-dessus de la zone */
|
||||
.pdf-dropzone.dragging {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
/* Un fichier valide a été sélectionné */
|
||||
.pdf-dropzone.has-file {
|
||||
border-style: solid;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Section Données (export / import) */
|
||||
.admin-data-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-data-title i {
|
||||
color: var(--accent-light);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.admin-data-text {
|
||||
margin: 0 0 16px;
|
||||
color: var(--text-muted);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.admin-data-warning {
|
||||
color: #ffb8bb;
|
||||
}
|
||||
|
||||
[data-theme="light"] .admin-data-warning {
|
||||
color: #b03538;
|
||||
}
|
||||
|
||||
.admin-export-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.admin-data-separator {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.project-form-card input[type="file"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--surface-2);
|
||||
border: 1px dashed var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-soft);
|
||||
font-family: var(--font);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ==== BOUTON FLOTTANT EXPÉRIENCES <-> ÉCOLES ==== */
|
||||
|
||||
.section-jump {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 200;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: linear-gradient(180deg, var(--accent) 0%, var(--accent-strong) 100%);
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 26px rgba(255, 122, 26, 0.4);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.section-jump:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 14px 32px rgba(255, 122, 26, 0.5);
|
||||
}
|
||||
|
||||
/* Par défaut (dans les expériences) : chapeau de diplômé = aller aux écoles */
|
||||
.section-jump .fa-briefcase {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Dans la section écoles : sac de travail = remonter aux expériences */
|
||||
.section-jump.mode-ecoles .fa-graduation-cap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section-jump.mode-ecoles .fa-briefcase {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* ==== NOTIFICATIONS TOAST ==== */
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 26px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: calc(100vw - 40px);
|
||||
padding: 15px 22px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--accent-border);
|
||||
border-radius: 14px;
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
box-shadow: var(--shadow);
|
||||
cursor: pointer;
|
||||
animation: toastIn 0.4s ease, toastOut 0.5s ease 4.5s forwards;
|
||||
}
|
||||
|
||||
.toast i {
|
||||
color: var(--success);
|
||||
font-size: 19px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(24px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(12px);
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==== ANIMATIONS & INTERACTIONS ==== */
|
||||
|
||||
/* Transition entre les pages : fondu de l'ancienne page vers la nouvelle
|
||||
@@ -1557,4 +2195,13 @@ main {
|
||||
.footer-links .discord-handle:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.scroll-indicator {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toast {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user