Compare commits
30
Commits
b2aefe3af5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c5ce36608 | ||
|
|
376a20966f | ||
|
|
d47258fad8 | ||
|
|
d7dc48454c | ||
|
|
6164f7eaae | ||
|
|
542910a15d | ||
|
|
a19c5c8381 | ||
|
|
88c10ee5b0 | ||
|
|
c2400d27b7 | ||
|
|
b5bacf04a9 | ||
|
|
87cc4e5e9c | ||
|
|
97caf2275c | ||
|
|
481fbc9498 | ||
|
|
6fb78e0bbb | ||
|
|
ba9bb21953 | ||
|
|
07bae3eff3 | ||
|
|
a019dcb00d | ||
|
|
e50eeacaeb | ||
|
|
9bbc05bcac | ||
|
|
612933f238 | ||
|
|
14f97564bf | ||
|
|
ac292fbcc4 | ||
|
|
4bab6548f7 | ||
|
|
57a34967bb | ||
|
|
12bba69908 | ||
|
|
3015c56583 | ||
|
|
5eef0d093c | ||
|
|
f043d8b611 | ||
|
|
965f537317 | ||
|
|
d67a479d10 |
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Config Claude Code
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
|
|
||||||
|
# Éditeurs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
Bienvenue sur ce portfolio pro et perso.
|
Bienvenue sur ce portfolio pro et perso.
|
||||||
|
|
||||||
Le projet est accessible à l'adresse suivante : https://esteban.eldorianet.work et est hébergé en France grâce à TerraTech Hebergement.
|
Le projet est accessible à l'adresse suivante : https://estebanadmin.fr et est hébergé en France grâce à TerraTech Hebergement.
|
||||||
|
|
||||||
## Réutilisation du projet
|
## Réutilisation du projet
|
||||||
|
|
||||||
La réutilisation du projet est autorisée, sous réserve qu'un fichier `idConnection.php` soit placé avant le dossier principal du site.
|
La réutilisation du projet est autorisée, sous réserve que les fichiers `idConnection.php` et `idDiscord.php` soient placés avant le dossier principal du site.
|
||||||
|
|
||||||
## Informations techniques
|
## Informations techniques
|
||||||
|
|
||||||
@@ -14,12 +14,6 @@ La réutilisation du projet est autorisée, sous réserve qu'un fichier `idConne
|
|||||||
- Le CSS a été généré avec l'aide d'une intelligence artificielle.
|
- Le CSS a été généré avec l'aide d'une intelligence artificielle.
|
||||||
- Le code PHP a été réalisé en grande partie par l'auteur.
|
- Le code PHP a été réalisé en grande partie par l'auteur.
|
||||||
|
|
||||||
## Sécurité
|
|
||||||
|
|
||||||
- Les mots de passe ne sont à ce stade ni hachés ni salés.
|
|
||||||
- Des failles de sécurité sont probablement encore présentes dans le système de connexion.
|
|
||||||
- Une amélioration de la sécurité du module de login est prévue prochainement.
|
|
||||||
|
|
||||||
## Signalement de vulnérabilité
|
## Signalement de vulnérabilité
|
||||||
|
|
||||||
Si vous identifiez une faille de sécurité ou un comportement suspect, merci de ne pas l'exploiter et de me contacter directement sur Discord : `@ours1`.
|
Si vous identifiez une faille de sécurité ou un comportement suspect, merci de ne pas l'exploiter et de me contacter directement sur Discord : `@ours1`.
|
||||||
+560
@@ -0,0 +1,560 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/functions/security.php');
|
||||||
|
|
||||||
|
// 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']);
|
||||||
|
header('Location: ./captcha.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le captcha est déjà validé, rediriger vers l'accueil
|
||||||
|
if (isset($_SESSION['captcha_valid']) && $_SESSION['captcha_valid'] === true) {
|
||||||
|
header('Location: ./index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nombre maximum d'essais
|
||||||
|
$maxAttempts = 3;
|
||||||
|
|
||||||
|
// Durée minimale (en secondes) entre l'affichage de la série et la soumission du formulaire.
|
||||||
|
// Un humain doit lire plusieurs messages avant de répondre : une soumission plus rapide
|
||||||
|
// que ça trahit presque toujours un robot qui remplit le formulaire instantanément.
|
||||||
|
$minSolveTime = 1.2;
|
||||||
|
|
||||||
|
// Liste des sujets normaux
|
||||||
|
$normalSubjects = [
|
||||||
|
'Votre rendez-vous est confirmé',
|
||||||
|
'Photo de famille',
|
||||||
|
'Invitation à déjeuner',
|
||||||
|
'Votre facture est disponible',
|
||||||
|
'Message de votre médecin',
|
||||||
|
'Votre colis arrivera demain',
|
||||||
|
'Reçu de paiement',
|
||||||
|
'Informations sur votre abonnement',
|
||||||
|
'Rappel de rendez-vous',
|
||||||
|
'Bulletin d’information',
|
||||||
|
'Horaire modifié',
|
||||||
|
'Demande de nouvelles',
|
||||||
|
'Anniversaire ce week-end',
|
||||||
|
'Confirmation de réservation',
|
||||||
|
'Mise à jour de votre compte',
|
||||||
|
'Message du voisin',
|
||||||
|
'Résultats d’analyse disponibles',
|
||||||
|
'Programme de la semaine',
|
||||||
|
'Votre commande a bien été envoyée',
|
||||||
|
'Merci pour votre visite',
|
||||||
|
'Document joint',
|
||||||
|
'Nouvelles de la famille',
|
||||||
|
'Convocation à la réunion',
|
||||||
|
'Changement d’horaire',
|
||||||
|
'Votre facture d’eau',
|
||||||
|
'Votre relevé est prêt',
|
||||||
|
'Confirmation de présence',
|
||||||
|
'Infos importantes pour demain',
|
||||||
|
'Rappel amical',
|
||||||
|
'Livraison prévue aujourd’hui',
|
||||||
|
'Votre réservation est confirmée',
|
||||||
|
'Message de la mairie',
|
||||||
|
'Avis de passage',
|
||||||
|
'Votre ticket est traité',
|
||||||
|
'Mise à jour de votre dossier',
|
||||||
|
'Message de l’école',
|
||||||
|
'Facture mensuelle',
|
||||||
|
'Invitation à la réunion',
|
||||||
|
'Confirmation de paiement',
|
||||||
|
'Votre commande est en route',
|
||||||
|
'Rappel de consultation',
|
||||||
|
'Nouvelle réponse reçue',
|
||||||
|
'Votre dossier a été mis à jour',
|
||||||
|
'Planning de la semaine',
|
||||||
|
'Reçu de réservation',
|
||||||
|
'Informations utiles',
|
||||||
|
'Votre assurance vous écrit',
|
||||||
|
'Message de votre pharmacien',
|
||||||
|
'Document disponible en ligne',
|
||||||
|
'Petit rappel important',
|
||||||
|
'Votre livraison est confirmée',
|
||||||
|
'Message du service client',
|
||||||
|
'Votre demande a été reçue',
|
||||||
|
'Lettre d’information',
|
||||||
|
'Nouvelles du jour',
|
||||||
|
'Confirmation de votre présence',
|
||||||
|
'Votre justificatif est prêt',
|
||||||
|
'Reçu de transaction',
|
||||||
|
'Votre compte a été mis à jour',
|
||||||
|
'Avis d’information',
|
||||||
|
'Votre dossier avance',
|
||||||
|
'Information concernant votre contrat',
|
||||||
|
'Message de votre banque',
|
||||||
|
'Votre échéance approche',
|
||||||
|
'Réponse à votre demande',
|
||||||
|
'Merci pour votre confiance',
|
||||||
|
'Votre pharmacie vous informe',
|
||||||
|
'Rappel de paiement',
|
||||||
|
'Votre colis a quitté l’entrepôt',
|
||||||
|
'Votre abonnement continue',
|
||||||
|
'Message de votre mutuelle',
|
||||||
|
'Invitation familiale',
|
||||||
|
'Nouveau message reçu',
|
||||||
|
'Votre place est réservée',
|
||||||
|
'Votre facture du mois',
|
||||||
|
'Compte-rendu envoyé',
|
||||||
|
'Votre demande est en cours',
|
||||||
|
'Rappel pour demain',
|
||||||
|
'Réception confirmée',
|
||||||
|
'Votre transport est réservé',
|
||||||
|
'Bonne réception',
|
||||||
|
'Votre document est validé',
|
||||||
|
'Votre suivi est disponible',
|
||||||
|
'Petit message amical',
|
||||||
|
'Confirmation de livraison',
|
||||||
|
'Votre note est disponible',
|
||||||
|
'Mise à jour d’informations',
|
||||||
|
'Votre service est activé',
|
||||||
|
'Votre message a bien été transmis',
|
||||||
|
'Reçu officiel',
|
||||||
|
'Votre planning a changé',
|
||||||
|
'Votre passage est confirmé',
|
||||||
|
'Information de dernière minute',
|
||||||
|
'Votre compte-rendu médical',
|
||||||
|
'Votre inscription est validée',
|
||||||
|
'Réponse automatique',
|
||||||
|
'Votre dossier est complet',
|
||||||
|
'Nouveau document reçu',
|
||||||
|
'Votre reçu est disponible',
|
||||||
|
'Votre commande est confirmée',
|
||||||
|
'Votre rendez-vous approche',
|
||||||
|
'Dernières informations'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Liste des expéditeurs normaux
|
||||||
|
$normalSenders = [
|
||||||
|
'Docteur Martin',
|
||||||
|
'Julie',
|
||||||
|
'Paul',
|
||||||
|
'Service client',
|
||||||
|
'École primaire',
|
||||||
|
'Mairie',
|
||||||
|
'Famille Dupont',
|
||||||
|
'Pharmacie du Centre',
|
||||||
|
'EDF Service',
|
||||||
|
'Voisinage',
|
||||||
|
'Transport local',
|
||||||
|
'Maison de santé',
|
||||||
|
'Claire Bernard',
|
||||||
|
'Assurance Habitation',
|
||||||
|
'Bibliothèque municipale',
|
||||||
|
'Banque familiale',
|
||||||
|
'Centre médical',
|
||||||
|
'La Poste',
|
||||||
|
'Mutuelle Santé',
|
||||||
|
'Service facturation'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Liste des sujets spam, chacun associé à un indice (raison) qui sera révélé après succès :
|
||||||
|
// une petite touche pédagogique sur les techniques de phishing les plus courantes
|
||||||
|
$spamSubjects = [
|
||||||
|
['text' => 'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURD’HUI', 'reason' => 'Urgence exagérée et abus des majuscules'],
|
||||||
|
['text' => 'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT', 'reason' => 'Gain surprise, trop beau pour être vrai'],
|
||||||
|
['text' => 'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE', 'reason' => 'Appel à l’action pressant et menaçant'],
|
||||||
|
['text' => 'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE', 'reason' => 'Demande de paiement immédiat inhabituelle'],
|
||||||
|
['text' => 'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE', 'reason' => 'Un service légitime ne redemande jamais un mot de passe par email'],
|
||||||
|
['text' => 'ACTION OBLIGATOIRE DANS LES 5 MINUTES', 'reason' => 'Délai artificiellement très court'],
|
||||||
|
['text' => 'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE', 'reason' => 'Usurpation d’un organisme bancaire'],
|
||||||
|
['text' => 'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT', 'reason' => 'Cadeau non sollicité'],
|
||||||
|
['text' => 'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER', 'reason' => 'Peur infondée utilisée pour pousser à agir vite'],
|
||||||
|
['text' => 'VOUS ÊTES LE GRAND GAGNANT DU JOUR', 'reason' => 'Gain surprise non sollicité'],
|
||||||
|
['text' => 'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE', 'reason' => 'Fausse alerte de paiement pour voler des données bancaires'],
|
||||||
|
['text' => 'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE', 'reason' => 'Menace de fermeture de compte'],
|
||||||
|
['text' => 'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER', 'reason' => 'Offre “secrète” artificiellement exclusive'],
|
||||||
|
['text' => 'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT', 'reason' => 'Fausse urgence de session'],
|
||||||
|
['text' => 'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI', 'reason' => 'Remboursement non sollicité'],
|
||||||
|
['text' => 'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES', 'reason' => 'Compte à rebours anxiogène'],
|
||||||
|
['text' => 'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE', 'reason' => 'Menace de suppression définitive'],
|
||||||
|
['text' => 'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ', 'reason' => 'Fausse alerte de piratage'],
|
||||||
|
['text' => 'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE', 'reason' => 'Demande d’informations bancaires par email'],
|
||||||
|
['text' => 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT', 'reason' => 'Promesse d’argent contre des données personnelles'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Liste des expéditeurs spam : mêmes formats d'affichage (nom, pas d'adresse brute) que les
|
||||||
|
// expéditeurs normaux ci-dessus, pour qu'il ne soit pas possible de repérer les mails
|
||||||
|
// frauduleux d'un simple coup d'œil sur l'expéditeur. La seule façon de les distinguer doit
|
||||||
|
// être de lire le contenu (sujet), comme un vrai email de phishing usurpant une identité connue.
|
||||||
|
$spamSenders = [
|
||||||
|
'Sécurité du compte',
|
||||||
|
'Service client',
|
||||||
|
'Banque familiale',
|
||||||
|
'Livraison Colis',
|
||||||
|
'Support technique',
|
||||||
|
'Service facturation',
|
||||||
|
'Assurance Habitation',
|
||||||
|
'Mutuelle Santé'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Liste des aperçus normaux
|
||||||
|
$normalPreviews = [
|
||||||
|
'Bonjour, nous vous confirmons votre rendez-vous prévu cette semaine.',
|
||||||
|
'Je vous envoie les informations demandées, bonne journée.',
|
||||||
|
'Votre document est prêt, vous pouvez le consulter.',
|
||||||
|
'Merci pour votre message, voici les détails utiles.',
|
||||||
|
'Votre commande a bien été prise en compte.',
|
||||||
|
'Petit rappel pour ne rien oublier demain.',
|
||||||
|
'Nous restons à votre disposition pour toute question.',
|
||||||
|
'Voici un message d’information concernant votre dossier.',
|
||||||
|
'Le service vous remercie et vous souhaite une bonne journée.',
|
||||||
|
'Vous trouverez ci-joint les éléments utiles.'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Liste des aperçus spam
|
||||||
|
$spamPreviews = [
|
||||||
|
'Cliquez tout de suite sinon votre compte sera fermé dans quelques minutes.',
|
||||||
|
'Paiement obligatoire immédiat pour débloquer la situation.',
|
||||||
|
'Confirmez vos données personnelles maintenant pour éviter la suppression.',
|
||||||
|
'Votre cadeau gratuit vous attend, cliquez ici sans attendre.',
|
||||||
|
'Action urgente requise, réponse dans les 5 minutes demandée.',
|
||||||
|
'Dernier avertissement officiel avant blocage définitif.'
|
||||||
|
];
|
||||||
|
|
||||||
|
function firstLetterSafe(string $text): string
|
||||||
|
{
|
||||||
|
if (function_exists('mb_substr') && function_exists('mb_strtoupper')) {
|
||||||
|
return mb_strtoupper(mb_substr($text, 0, 1, 'UTF-8'), 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtoupper(substr($text, 0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construit une nouvelle série de mails pour une manche de captcha.
|
||||||
|
// Le nombre de mails frauduleux varie entre 1 et 3 : un robot ne peut donc pas se
|
||||||
|
// contenter de "toujours cocher une seule case au hasard", ce qui réduit fortement
|
||||||
|
// les chances de réussir par pure chance (voir le calcul dans la doc du projet).
|
||||||
|
function buildInboxRound(
|
||||||
|
array $normalSubjects,
|
||||||
|
array $normalSenders,
|
||||||
|
array $normalPreviews,
|
||||||
|
array $spamSubjects,
|
||||||
|
array $spamSenders,
|
||||||
|
array $spamPreviews,
|
||||||
|
int $maxAttempts
|
||||||
|
): array {
|
||||||
|
$normalPool = [];
|
||||||
|
foreach ($normalSubjects as $index => $subject) {
|
||||||
|
$normalPool[] = [
|
||||||
|
'id' => 'normal_' . $index,
|
||||||
|
'sender' => $normalSenders[array_rand($normalSenders)],
|
||||||
|
'subject' => $subject,
|
||||||
|
'preview' => $normalPreviews[array_rand($normalPreviews)],
|
||||||
|
'is_spam' => false,
|
||||||
|
'reason' => null,
|
||||||
|
'time' => sprintf('%02d:%02d', random_int(8, 19), random_int(0, 59))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$spamPool = [];
|
||||||
|
foreach ($spamSubjects as $index => $spam) {
|
||||||
|
$spamPool[] = [
|
||||||
|
'id' => 'spam_' . $index,
|
||||||
|
'sender' => $spamSenders[array_rand($spamSenders)],
|
||||||
|
'subject' => $spam['text'],
|
||||||
|
'preview' => $spamPreviews[array_rand($spamPreviews)],
|
||||||
|
'is_spam' => true,
|
||||||
|
'reason' => $spam['reason'],
|
||||||
|
'time' => sprintf('%02d:%02d', random_int(0, 23), random_int(0, 59))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffle($normalPool);
|
||||||
|
shuffle($spamPool);
|
||||||
|
|
||||||
|
// Entre 1 et 3 mails frauduleux dans les 10 affichés (voir commentaire de la fonction)
|
||||||
|
$spamCount = random_int(1, 3);
|
||||||
|
$selectedSpam = array_slice($spamPool, 0, $spamCount);
|
||||||
|
$selectedNormal = array_slice($normalPool, 0, 10 - $spamCount);
|
||||||
|
|
||||||
|
$selected = array_merge($selectedSpam, $selectedNormal);
|
||||||
|
shuffle($selected);
|
||||||
|
|
||||||
|
$correctIds = array_map(function ($mail) {
|
||||||
|
return $mail['id'];
|
||||||
|
}, $selectedSpam);
|
||||||
|
sort($correctIds);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => bin2hex(random_bytes(16)),
|
||||||
|
'correct_ids' => $correctIds,
|
||||||
|
'emails' => $selected,
|
||||||
|
'attempts_left' => $maxAttempts,
|
||||||
|
'started_at' => microtime(true)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_SESSION['captcha_round']) || !is_array($_SESSION['captcha_round'])) {
|
||||||
|
$_SESSION['captcha_round'] = buildInboxRound(
|
||||||
|
$normalSubjects,
|
||||||
|
$normalSenders,
|
||||||
|
$normalPreviews,
|
||||||
|
$spamSubjects,
|
||||||
|
$spamSenders,
|
||||||
|
$spamPreviews,
|
||||||
|
$maxAttempts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$round = $_SESSION['captcha_round'];
|
||||||
|
$error = '';
|
||||||
|
$success = false;
|
||||||
|
$locked = false;
|
||||||
|
$foundReasons = [];
|
||||||
|
|
||||||
|
// Regénère une série si la structure en session est invalide ou date d'avant cette mise à
|
||||||
|
// jour (ancienne session sans correct_ids/started_at) : évite tout crash après déploiement
|
||||||
|
if (
|
||||||
|
!isset($round['emails']) || !is_array($round['emails']) ||
|
||||||
|
!isset($round['correct_ids']) || !is_array($round['correct_ids']) ||
|
||||||
|
!isset($round['started_at'])
|
||||||
|
) {
|
||||||
|
$_SESSION['captcha_round'] = buildInboxRound(
|
||||||
|
$normalSubjects,
|
||||||
|
$normalSenders,
|
||||||
|
$normalPreviews,
|
||||||
|
$spamSubjects,
|
||||||
|
$spamSenders,
|
||||||
|
$spamPreviews,
|
||||||
|
$maxAttempts
|
||||||
|
);
|
||||||
|
$round = $_SESSION['captcha_round'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion de la soumission du formulaire
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$postedToken = $_POST['captcha_token'] ?? '';
|
||||||
|
|
||||||
|
$selectedMails = (isset($_POST['selected_mails']) && is_array($_POST['selected_mails']))
|
||||||
|
? array_map('strval', $_POST['selected_mails'])
|
||||||
|
: [];
|
||||||
|
sort($selectedMails);
|
||||||
|
|
||||||
|
// Honeypot : champ invisible pour un humain (masqué en CSS), que les robots qui
|
||||||
|
// remplissent tous les champs d'un formulaire ont tendance à compléter malgré tout
|
||||||
|
$honeypot = trim($_POST['website'] ?? '');
|
||||||
|
|
||||||
|
if (!hash_equals($round['token'], $postedToken)) {
|
||||||
|
$error = 'Session invalide, recharge la page.';
|
||||||
|
} elseif (($round['attempts_left'] ?? 0) <= 0) {
|
||||||
|
$locked = true;
|
||||||
|
$error = 'Tu n’as plus d’essais. Recharge une nouvelle série.';
|
||||||
|
} elseif ($honeypot !== '') {
|
||||||
|
// On fait échouer la tentative sans révéler la vraie raison, pour ne pas
|
||||||
|
// donner d'indice à un robot sur la façon dont il a été détecté
|
||||||
|
$_SESSION['captcha_round']['attempts_left']--;
|
||||||
|
$round = $_SESSION['captcha_round'];
|
||||||
|
$locked = ($round['attempts_left'] ?? 0) <= 0;
|
||||||
|
$error = $locked
|
||||||
|
? 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.'
|
||||||
|
: 'Ce n’est pas la bonne sélection. Regarde mieux les messages les plus agressifs ou trop urgents.';
|
||||||
|
sendLog('Captcha : champ piège rempli (robot probable)');
|
||||||
|
} elseif ((microtime(true) - (float) $round['started_at']) < $minSolveTime) {
|
||||||
|
// Une réponse aussi rapide n'est pas plausible pour un humain qui doit lire 10 mails
|
||||||
|
$_SESSION['captcha_round']['attempts_left']--;
|
||||||
|
$round = $_SESSION['captcha_round'];
|
||||||
|
$locked = ($round['attempts_left'] ?? 0) <= 0;
|
||||||
|
$error = $locked
|
||||||
|
? 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.'
|
||||||
|
: 'Ce n’est pas la bonne sélection. Prends le temps de lire les messages.';
|
||||||
|
sendLog('Captcha : soumission trop rapide (robot probable)');
|
||||||
|
} elseif ($selectedMails !== $round['correct_ids']) {
|
||||||
|
$_SESSION['captcha_round']['attempts_left']--;
|
||||||
|
$round = $_SESSION['captcha_round'];
|
||||||
|
|
||||||
|
if (($round['attempts_left'] ?? 0) <= 0) {
|
||||||
|
$locked = true;
|
||||||
|
$error = 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.';
|
||||||
|
sendLog('Captcha raté : plus aucun essai restant (possible robot)');
|
||||||
|
} else {
|
||||||
|
$error = 'Ce n’est pas la bonne sélection. Regarde mieux les messages les plus agressifs ou trop urgents.';
|
||||||
|
sendLog('Captcha raté : mauvaise sélection (' . $round['attempts_left'] . ' essai(s) restant(s))');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$_SESSION['captcha_valid'] = true;
|
||||||
|
|
||||||
|
// On récupère les explications des mails trouvés pour les afficher sur
|
||||||
|
// l'écran de succès (petit bonus pédagogique) avant de vider la manche
|
||||||
|
foreach ($round['emails'] as $mail) {
|
||||||
|
if ($mail['is_spam'] && !empty($mail['reason'])) {
|
||||||
|
$foundReasons[] = $mail['reason'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($_SESSION['captcha_round']);
|
||||||
|
$success = true;
|
||||||
|
sendLog('Captcha réussi : nouveau visiteur sur le site');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$attemptsLeft = $round['attempts_left'] ?? 0;
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<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">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="./styles/captcha.css">
|
||||||
|
</head>
|
||||||
|
<body class="<?php echo $success ? 'success-mode' : ''; ?>">
|
||||||
|
<main class="captcha-shell">
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<section class="success-screen">
|
||||||
|
<div class="success-burst"></div>
|
||||||
|
<div class="success-icon">✅</div>
|
||||||
|
<h1>Vérification réussie</h1>
|
||||||
|
<p>Bravo, tu as repéré le(s) mail(s) frauduleux. Redirection en cours...</p>
|
||||||
|
|
||||||
|
<?php if (!empty($foundReasons)): ?>
|
||||||
|
<div class="success-reasons">
|
||||||
|
<p class="success-reasons-title">Ce que tu as repéré :</p>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($foundReasons as $reason): ?>
|
||||||
|
<li><i class="reason-dot"></i><?php echo htmlspecialchars($reason, ENT_QUOTES, 'UTF-8'); ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="success-loader">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
setTimeout(function () {
|
||||||
|
window.location.href = './index.php';
|
||||||
|
}, 2400);
|
||||||
|
</script>
|
||||||
|
<?php else: ?>
|
||||||
|
<form class="captcha-card" method="post" action="">
|
||||||
|
<div class="captcha-badge">🛡️ Test anti-robot</div>
|
||||||
|
|
||||||
|
<div class="captcha-top">
|
||||||
|
<div>
|
||||||
|
<h1 class="captcha-title">Vérification humaine</h1>
|
||||||
|
<p class="captcha-subtitle">
|
||||||
|
Sélectionne <strong>tous les emails qui te semblent frauduleux</strong> parmi les messages
|
||||||
|
ci-dessous. Certains sont plus dangereux que d'autres.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="captcha-target">
|
||||||
|
<span>Vérification demandée</span>
|
||||||
|
<strong>Repérer le phishing</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="captcha-stats">
|
||||||
|
<div class="captcha-attempts">
|
||||||
|
<span>Essais restants</span>
|
||||||
|
<strong><?php echo htmlspecialchars((string) $attemptsLeft, ENT_QUOTES, 'UTF-8'); ?> / <?php echo htmlspecialchars((string) $maxAttempts, ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||||
|
<div class="attempts-dots">
|
||||||
|
<?php for ($i = 0; $i < $maxAttempts; $i++): ?>
|
||||||
|
<span class="<?php echo $i < $attemptsLeft ? 'dot-filled' : 'dot-empty'; ?>"></span>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($locked): ?>
|
||||||
|
<a class="captcha-reset" href="./captcha.php?reload=1">Recharger une nouvelle série</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($error !== ''): ?>
|
||||||
|
<div class="captcha-error"><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="mailbox">
|
||||||
|
<div class="mailbox-toolbar">
|
||||||
|
<div class="mailbox-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mailbox-label">Test anti-robot : cochez les mails qui ressemblent à une arnaque</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mail-list">
|
||||||
|
<?php foreach ($round['emails'] as $index => $mail): ?>
|
||||||
|
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>" style="--mail-index: <?php echo (int) $index; ?>">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="selected_mails[]"
|
||||||
|
value="<?php echo htmlspecialchars($mail['id'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
<?php echo $locked ? 'disabled' : ''; ?>
|
||||||
|
>
|
||||||
|
<span class="mail-row">
|
||||||
|
<span class="mail-check"><i class="fa-solid fa-check"></i></span>
|
||||||
|
|
||||||
|
<span class="mail-avatar">
|
||||||
|
<?php echo htmlspecialchars(firstLetterSafe($mail['sender']), ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="mail-sender">
|
||||||
|
<?php echo htmlspecialchars($mail['sender'], ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="mail-content">
|
||||||
|
<span class="mail-subject">
|
||||||
|
<?php echo htmlspecialchars($mail['subject'], ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</span>
|
||||||
|
<span class="mail-preview">
|
||||||
|
<?php echo htmlspecialchars($mail['preview'], ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="mail-time">
|
||||||
|
<?php echo htmlspecialchars($mail['time'], ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="captcha_token"
|
||||||
|
value="<?php echo htmlspecialchars($round['token'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Champ piège (honeypot) : invisible et exclu du tabulateur pour un humain,
|
||||||
|
mais souvent rempli automatiquement par les robots qui remplissent tout -->
|
||||||
|
<div class="captcha-honeypot" aria-hidden="true">
|
||||||
|
<label for="website">Laisse ce champ vide</label>
|
||||||
|
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="captcha-footer">
|
||||||
|
<p class="captcha-note">
|
||||||
|
Tu as plusieurs essais. Les faux mails utilisent souvent l’urgence, les cadeaux improbables, les menaces ou les demandes de mot de passe.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="captcha-actions">
|
||||||
|
<?php if ($locked): ?>
|
||||||
|
<a class="captcha-secondary-button" href="./captcha.php?reload=1">Nouvelle série</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<button type="submit" class="captcha-submit">Valider ma sélection</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+109
-19
@@ -1,52 +1,142 @@
|
|||||||
-- On créer la base de donnée si elle n'existe déjà aps
|
CREATE DATABASE IF NOT EXISTS portfolio;
|
||||||
CREATE DATABASE IF NOT EXISTS portfolio
|
USE portfolio;
|
||||||
|
|
||||||
-- On créer un utilisateur avec un mot de passe fort qui est à changer après execution dus script (voir bitwarden)
|
-- Mot de passe à changer après exécution du script (voir Bitwarden)
|
||||||
CREATE USER 'srv_portfolio'@'localhost' IDENTIFIED BY 'mdp';
|
CREATE USER IF NOT EXISTS 'srv_portfolio'@'%' IDENTIFIED BY 'mdp';
|
||||||
|
GRANT ALL ON portfolio.* TO 'srv_portfolio'@'%';
|
||||||
-- On met les droits au compte sur la base portfolio à l'utilisateur
|
|
||||||
GRANT ALL ON portfolio.* TO 'srv_portfolio'@'localhost';
|
|
||||||
|
|
||||||
-- On actualise les permissions
|
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
-- Création de la table user : (Généré par Beekeeper Studio : équivalent à dbGate)
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
username VARCHAR(50) NOT NULL,
|
username VARCHAR(50) NOT NULL,
|
||||||
password VARCHAR(255) NOT NULL, -- Je laisse ici de la place pour prévoir un futur hashage et sallage
|
password VARCHAR(255) NOT NULL,
|
||||||
role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
|
role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
|
||||||
created_at DATETIME NULL DEFAULT NULL,
|
created_at DATETIME NULL DEFAULT NULL,
|
||||||
last_login DATETIME NULL DEFAULT NULL,
|
last_login DATETIME NULL DEFAULT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE INDEX idx_username (username)
|
UNIQUE INDEX idx_username (username)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Création de la table projects : (Généré par Beekeeper Studio)
|
INSERT IGNORE INTO users (id, created_at, last_login, password, role, username)
|
||||||
|
VALUES (11, '2026-06-08 11:10:21', '2026-06-08 13:10:41', '$2y$12$x01fDqrHQEYIyj7MDHa72eRyl2j/n5cNq9sv3.FIiQCaRsB50Q4iK', 'admin', 'admin');
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS projects (
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
title VARCHAR(100) NOT NULL,
|
title VARCHAR(100) NOT NULL,
|
||||||
|
status VARCHAR(100) NULL,
|
||||||
tags VARCHAR(255) NULL,
|
tags VARCHAR(255) NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
image_url VARCHAR(255) NOT NULL, -- Stockage de l'image par URL (exemple : par CDN)
|
image_url VARCHAR(255) NOT NULL,
|
||||||
|
markdown LONGTEXT NULL,
|
||||||
|
link_url VARCHAR(255) NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Création de la table projects : (Généré par Beekeeper Studio)
|
|
||||||
CREATE TABLE IF NOT EXISTS experiences (
|
CREATE TABLE IF NOT EXISTS experiences (
|
||||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
title VARCHAR(100) NOT NULL,
|
title VARCHAR(100) NOT NULL,
|
||||||
|
status VARCHAR(100) NULL,
|
||||||
xptype VARCHAR(100) NOT NULL,
|
xptype VARCHAR(100) NOT NULL,
|
||||||
tags VARCHAR(255) NULL,
|
tags VARCHAR(255) NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
duration VARCHAR(100) NOT NULL,
|
duration VARCHAR(100) NOT NULL,
|
||||||
image_url VARCHAR(255) NOT NULL, -- Stockage de l'image par URL (exemple : par CDN)
|
image_url VARCHAR(255) NOT NULL,
|
||||||
|
markdown LONGTEXT NULL,
|
||||||
|
link_url VARCHAR(255) NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Compte de test
|
CREATE TABLE IF NOT EXISTS schools (
|
||||||
INSERT INTO users (username, password, role)
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
VALUES ('admin', 'admin', 'admin');
|
title VARCHAR(100) NOT NULL,
|
||||||
|
status VARCHAR(100) NULL,
|
||||||
|
tags VARCHAR(255) NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
duration VARCHAR(100) NOT NULL,
|
||||||
|
image_url VARCHAR(255) NOT NULL,
|
||||||
|
markdown LONGTEXT NULL,
|
||||||
|
link_url VARCHAR(255) NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Réglages du site (clé / valeur), utilisés pour la notification de la page d'accueil et le captcha
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
name VARCHAR(50) NOT NULL,
|
||||||
|
value TEXT NULL,
|
||||||
|
PRIMARY KEY (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO settings (name, value) VALUES
|
||||||
|
('site_notice_title', 'Des bugs peuvent encore se produire.'),
|
||||||
|
('site_notice_text', 'Si le CSS ou les images s''affichent mal, pensez à vider le cache de votre navigateur (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications.'),
|
||||||
|
('site_notice_enabled', '1'),
|
||||||
|
('captcha_enabled', '1'),
|
||||||
|
('contact_email', 'esteban.moussu@ik.me'),
|
||||||
|
('contact_email_enabled', '1'),
|
||||||
|
('contact_address_line1', '20 Grande rue'),
|
||||||
|
('contact_address_line2', '52500 Broncourt'),
|
||||||
|
('contact_address_enabled', '1')
|
||||||
|
ON DUPLICATE KEY UPDATE value = VALUES(value);
|
||||||
|
|
||||||
|
-- items : un item par ligne, au format "classes-icone|Texte" (icône optionnelle)
|
||||||
|
CREATE TABLE IF NOT EXISTS skills (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
title VARCHAR(100) NOT NULL,
|
||||||
|
icon VARCHAR(100) NULL,
|
||||||
|
items TEXT NOT NULL,
|
||||||
|
position INT NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO skills (title, icon, items, position) VALUES
|
||||||
|
('Systèmes d''exploitation', 'fa-solid fa-computer', 'fa-brands fa-microsoft|Windows (Serveur/XP/8/10/11)\nfa-brands fa-linux|Linux (Debian/Ubuntu)\nfa-brands fa-apple|macOS', 1),
|
||||||
|
('Réseaux', 'fa-solid fa-network-wired', 'fa-solid fa-location-dot|Adressage IP\nfa-solid fa-diagram-project|VLAN\nfa-solid fa-shield-halved|Pare-feu (iptables/ufw)\nfa-solid fa-globe|DNS (Bind9)\nfa-solid fa-server|DHCP (ISC DHCP)\nfa-solid fa-arrows-left-right|Proxy (Squid)', 2),
|
||||||
|
('Scripting', 'fa-solid fa-terminal', 'fa-solid fa-terminal|Bash\nfa-brands fa-python|Python\nfa-brands fa-java|Java\nfa-solid fa-gears|Ansible', 3),
|
||||||
|
('Applications', 'fa-solid fa-cubes', 'fa-solid fa-address-book|LDAP (Windows AD)\nfa-solid fa-headset|GLPI\nfa-solid fa-compact-disc|FOG\nfa-solid fa-server|Nginx / Apache\nfa-brands fa-docker|Docker\nfa-solid fa-display|Guacamole\nfa-solid fa-database|MySQL / MariaDB', 4),
|
||||||
|
('Développement web', 'fa-solid fa-code', 'fa-brands fa-html5|HTML\nfa-brands fa-css3-alt|CSS\nfa-brands fa-js|JavaScript\nfa-brands fa-php|PHP', 5),
|
||||||
|
('Virtualisation & Outils', 'fa-solid fa-layer-group', 'fa-solid fa-server|Proxmox\nfa-solid fa-box|VirtualBox\nfa-brands fa-git-alt|Git (GitHub, Gitea)', 6);
|
||||||
|
|
||||||
|
-- category : 'tool' (bandeau "Outils que je sais manipuler") ou 'partner' (bandeau "Ils m'ont fait confiance")
|
||||||
|
-- icon : classes CSS Devicon, prioritaire sur logo_url si les deux sont renseignés (au moins un des deux requis)
|
||||||
|
CREATE TABLE IF NOT EXISTS logo_banners (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
category ENUM('tool', 'partner') NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
icon VARCHAR(100) NULL,
|
||||||
|
logo_url VARCHAR(255) NULL,
|
||||||
|
link_url VARCHAR(255) NULL,
|
||||||
|
position INT NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- PDF de projects/experiences/schools : tables associées plutôt qu'une colonne sur la table
|
||||||
|
-- parente, pour que les pages qui LISTENT ces éléments n'aient jamais à charger le contenu
|
||||||
|
-- binaire des PDF en mémoire (seul le nom de fichier y est utilisé)
|
||||||
|
CREATE TABLE IF NOT EXISTS project_pdfs (
|
||||||
|
project_id INT UNSIGNED NOT NULL,
|
||||||
|
pdf_filename VARCHAR(255) NOT NULL,
|
||||||
|
pdf_data LONGBLOB NOT NULL,
|
||||||
|
PRIMARY KEY (project_id),
|
||||||
|
CONSTRAINT fk_project_pdfs_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS experience_pdfs (
|
||||||
|
experience_id INT UNSIGNED NOT NULL,
|
||||||
|
pdf_filename VARCHAR(255) NOT NULL,
|
||||||
|
pdf_data LONGBLOB NOT NULL,
|
||||||
|
PRIMARY KEY (experience_id),
|
||||||
|
CONSTRAINT fk_experience_pdfs_experience FOREIGN KEY (experience_id) REFERENCES experiences(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS school_pdfs (
|
||||||
|
school_id INT UNSIGNED NOT NULL,
|
||||||
|
pdf_filename VARCHAR(255) NOT NULL,
|
||||||
|
pdf_data LONGBLOB NOT NULL,
|
||||||
|
PRIMARY KEY (school_id),
|
||||||
|
CONSTRAINT fk_school_pdfs_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
; Config PHP pour le conteneur Docker (image php:8.2-apache)
|
||||||
|
; Copiée dans /usr/local/etc/php/conf.d/ par le Dockerfile généré par scriptsite.sh
|
||||||
|
|
||||||
|
; Augmente les limites d'upload pour permettre le dépôt de PDF depuis le panel admin
|
||||||
|
; post_max_size doit toujours être >= upload_max_filesize
|
||||||
|
upload_max_filesize = 20M
|
||||||
|
post_max_size = 20M
|
||||||
|
|
||||||
|
; Coupe l'affichage des erreurs PHP dans le HTML en production (sécurité : ne pas exposer
|
||||||
|
; les chemins serveur aux visiteurs) et les envoie dans les logs Apache/Docker à la place.
|
||||||
|
; Bonus : évite les warnings HTML qui provoquaient "headers already sent" une fois affichés
|
||||||
|
; avant le session_start()/header() des scripts.
|
||||||
|
display_errors = Off
|
||||||
|
log_errors = On
|
||||||
+61
-4
@@ -1,21 +1,78 @@
|
|||||||
<?php
|
<?php
|
||||||
// Ce Footer sera inseré sur des pages grâce a la fonction require_once('./../elements/footer.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
|
// 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>
|
</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">
|
<footer class="site-footer">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<p class="footer-copyright">
|
<p class="footer-copyright">
|
||||||
<!-- Logo droit d'auteur + année -->
|
<!-- Logo droit d'auteur + année -->
|
||||||
© <?php echo date('Y'); ?> - Portfolio développé par Moussu Esteban
|
© <?php echo date('Y'); ?> - Portfolio développé par Esteban MOUSSU
|
||||||
</p>
|
</p>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="https://www.linkedin.com/in/estebanmoussu/" target="_blank" rel="noopener noreferrer">LinkedIn</a>
|
<a href="https://www.linkedin.com/in/estebanmoussu/" target="_blank" rel="noopener noreferrer">LinkedIn</a>
|
||||||
<a href="https://www.instagram.com/ours_one_" target="_blank" rel="noopener noreferrer">Instagram</a>
|
<a href="https://www.instagram.com/ours_one_" target="_blank" rel="noopener noreferrer">Instagram</a>
|
||||||
<a href="https://git.eldorianet.work/esteban" target="_blank" rel="noopener noreferrer">Git</a>
|
<a href="https://git.eldorianet.work/esteban" target="_blank" rel="noopener noreferrer">Git</a>
|
||||||
<span class="discord-handle" title="@ours1" onclick="alert('Discord : @ours1')">Discord</span>
|
<span class="discord-handle" title="@ours1" onclick="alert('Discord : @ours1')">Discord</span>
|
||||||
<a href="../pages/cgu.php">Conditions générales / Utilisations</a>
|
<a href="../pages/cgu.php">CGU</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Apparition des cartes au défilement (IntersectionObserver) -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
// On respecte la préférence "réduire les animations" du système
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||||
|
if (!('IntersectionObserver' in window)) return;
|
||||||
|
|
||||||
|
// Éléments qui apparaissent en glissant vers le haut quand ils entrent dans l'écran
|
||||||
|
var cibles = document.querySelectorAll(
|
||||||
|
'.project-card, .quick-link-card, .skill-category, .contact-card, ' +
|
||||||
|
'.login-card, .project-form-card, .cgu-card, .logo-admin-item, ' +
|
||||||
|
'.page-header, .quick-links-header, .skills-header, .projects-title, .logo-banner-header, ' +
|
||||||
|
'.stat-item'
|
||||||
|
);
|
||||||
|
|
||||||
|
var observer = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (entry) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add('visible');
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
|
||||||
|
|
||||||
|
cibles.forEach(function (el, i) {
|
||||||
|
el.classList.add('reveal');
|
||||||
|
// Petit décalage en cascade pour les éléments d'une même grille
|
||||||
|
el.style.setProperty('--reveal-delay', ((i % 4) * 0.08) + 's');
|
||||||
|
observer.observe(el);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+101
-18
@@ -1,33 +1,102 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Ce Header sera inseré sur des pages grâce a la fonction require_once('./../elements/header.php')
|
// Ce Header sera inseré sur des pages grâce a la fonction require_once('./../elements/header.php')
|
||||||
// Rappel des différentes pages : Acceuil, Projets, Contact
|
// Rappel des différentes pages : Acceuil, Projets, Contact
|
||||||
// Rappel des boutons : Connexion, (Si le temps) Panneau Administrateur (Seulement si l'utilisateur est admin)
|
// Rappel des boutons : Connexion, (Si le temps) Panneau Administrateur (Seulement si l'utilisateur est admin)
|
||||||
|
|
||||||
|
|
||||||
// Ajout ici d'un démarrage de session : header étant dans toutes les pages
|
// Ajout ici d'un démarrage de session : header étant dans toutes les pages
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
|
||||||
|
// Ajout ici la vérification du captcha
|
||||||
|
require_once('../scripts/requireCaptcha.php');
|
||||||
|
|
||||||
// Ajout des logs sur toutes les pages
|
// Ajout des logs sur toutes les pages
|
||||||
require_once('../functions/sendLogs.php');
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
// Ajout du CSS Primaire (fait par IA)
|
// 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';
|
||||||
|
|
||||||
|
// Description : chaque page peut définir $pageDescription avant d'inclure le header
|
||||||
|
// (sinon description générique, utilisée pour le SEO et les aperçus de partage)
|
||||||
|
$pageDescription = isset($pageDescription)
|
||||||
|
? $pageDescription
|
||||||
|
: 'Portfolio d\'Esteban MOUSSU, étudiant en informatique passionné par les systèmes, les réseaux et le développement web. Projets, expériences et compétences.';
|
||||||
|
|
||||||
|
// URL absolue de la page courante (pour og:url et le lien canonique)
|
||||||
|
$pageUrl = 'https://estebanadmin.fr' . strtok($_SERVER['REQUEST_URI'], '?');
|
||||||
|
|
||||||
|
// Page courante pour souligner le lien actif dans la navigation
|
||||||
|
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||||
?>
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<!-- Meta viewport : indispensable pour que le site soit responsive sur mobile -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php echo htmlspecialchars($pageTitle); ?></title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="../styles/primary_css.css">
|
<!-- SEO : description + lien canonique -->
|
||||||
|
<meta name="description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||||
|
<link rel="canonical" href="<?php echo htmlspecialchars($pageUrl); ?>">
|
||||||
|
|
||||||
|
<!-- Aperçu de partage (réseaux sociaux, Discord, etc.) -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="<?php echo htmlspecialchars($pageTitle); ?>">
|
||||||
|
<meta property="og:description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||||
|
<meta property="og:url" content="<?php echo htmlspecialchars($pageUrl); ?>">
|
||||||
|
<meta property="og:image" content="https://estebanadmin.fr/memoji.png">
|
||||||
|
<meta property="og:locale" content="fr_FR">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="<?php echo htmlspecialchars($pageTitle); ?>">
|
||||||
|
<meta name="twitter:description" content="<?php echo htmlspecialchars($pageDescription); ?>">
|
||||||
|
<meta name="twitter:image" content="https://estebanadmin.fr/memoji.png">
|
||||||
|
|
||||||
|
<!-- Favicon du site -->
|
||||||
|
<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 () {
|
||||||
|
if (localStorage.getItem('theme') === 'light') {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Police moderne Inter (Google Fonts) -->
|
||||||
|
<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">
|
||||||
|
|
||||||
|
<!-- Ajout du CSS Primaire (fait par IA) -->
|
||||||
|
<link rel="stylesheet" href="../styles/primary_css.css">
|
||||||
|
<!-- Ajout de Font Awesome pour les icones de home.php-->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||||
|
<!-- Devicon : équivalent gratuit de Font Awesome dédié aux logos d'outils/langages/frameworks,
|
||||||
|
utilisé pour le bandeau "Outils que je sais manipuler" de la page d'accueil -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/devicon@2.15.1/devicon.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
<!-- Header + nav -->
|
<!-- Header + nav -->
|
||||||
<header>
|
<header class="site-header">
|
||||||
<!-- Texte qui sera dans le header -->
|
<!-- Texte qui sera dans le header -->
|
||||||
<a href="./../index.php" class="titre-de-page">Esteban MOUSSU - PortFolio</a>
|
<a href="./../index.php" class="titre-de-page">Esteban MOUSSU - PortFolio</a>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
|
||||||
<!-- Module de navigation -->
|
<!-- Module de navigation -->
|
||||||
<nav>
|
<nav class="site-nav">
|
||||||
<a href="./../index.php">Accueil</a>
|
<a href="./../pages/home.php" class="<?php echo ($currentPage === 'home.php') ? 'active' : ''; ?>">Accueil</a>
|
||||||
<a href="./projects.php">Projets</a>
|
<a href="./../pages/projects.php" class="<?php echo ($currentPage === 'projects.php') ? 'active' : ''; ?>">Projets</a>
|
||||||
<a href="./xp.php">Expériences</a>
|
<a href="./../pages/xp.php" class="<?php echo ($currentPage === 'xp.php') ? 'active' : ''; ?>">Expériences</a>
|
||||||
<a href="./contact.php">Contact</a>
|
<a href="./../pages/skills.php" class="<?php echo ($currentPage === 'skills.php') ? 'active' : ''; ?>">Compétences</a>
|
||||||
|
<a href="./../pages/contact.php" class="<?php echo ($currentPage === 'contact.php') ? 'active' : ''; ?>">Contact</a>
|
||||||
<?php
|
<?php
|
||||||
// Vérification que l'utilisateur est admin, s'il est admin, il aura accès au panel admin
|
// Vérification que l'utilisateur est admin, s'il est admin, il aura accès au panel admin
|
||||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||||
@@ -35,29 +104,43 @@ require_once('../functions/sendLogs.php');
|
|||||||
} else {
|
} else {
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<a href="./admin.php">Administration</a>
|
<!-- Lien vers le panneau admin -->
|
||||||
|
<a href="./../pages/admin.php" class="<?php echo ($currentPage === 'admin.php') ? 'active' : ''; ?>">Administration</a>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Vérification si l'utilisateur est connecté, si c'est le cas au lieu de Connexion il aura Deconnexion
|
// Vérification si l'utilisateur est connecté, si c'est le cas au lieu de Connexion il aura Deconnexion
|
||||||
|
|
||||||
if (isset($_SESSION['user_id'])){
|
if (isset($_SESSION['user_id'])){
|
||||||
?>
|
?>
|
||||||
<a href="../scripts/disconnect.php">Deconnexion</a>
|
<a href="../scripts/disconnect.php" class="nav-cta">Deconnexion</a>
|
||||||
<?php
|
<?php
|
||||||
} else {
|
} else {
|
||||||
?>
|
?>
|
||||||
<a href="./login.php">Connexion</a>
|
<a href="./../pages/login.php" class="nav-cta <?php echo ($currentPage === 'login.php') ? 'active' : ''; ?>">Connexion</a>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Fonctionnalité future changement de thème dark / light et panneau admin -->
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Bouton de changement de thème dark / light : le choix est mémorisé dans le navigateur (localStorage) -->
|
||||||
|
<button class="theme-toggle" aria-label="Changer de thème"
|
||||||
|
onclick="var h=document.documentElement;var l=h.getAttribute('data-theme')==='light';if(l){h.removeAttribute('data-theme');localStorage.setItem('theme','dark');}else{h.setAttribute('data-theme','light');localStorage.setItem('theme','light');}">
|
||||||
|
<i class="fa-solid fa-sun"></i><i class="fa-solid fa-moon"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Bouton burger : visible uniquement sur mobile, ouvre/ferme la navigation -->
|
||||||
|
<button class="nav-toggle" aria-label="Ouvrir le menu" aria-expanded="false"
|
||||||
|
onclick="var h=document.querySelector('.site-header');h.classList.toggle('nav-open');this.setAttribute('aria-expanded',h.classList.contains('nav-open'));">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Ajout de connexion a la bdd
|
||||||
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
|
// Fonction qui va faire une requête SQL afin de collecter les logos
|
||||||
|
// $category vaut 'tool', 'partner', ou null pour récupérer les deux catégories
|
||||||
|
// (toutes catégories confondues, utilisé par le panel admin)
|
||||||
|
// Retourne un tableau vide si la table n'existe pas encore (pas de die() pour que la page
|
||||||
|
// d'accueil continue de fonctionner tant que le script SQL n'a pas été exécuté)
|
||||||
|
function listeLogos($category = null) {
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// SELECT * : la colonne icon est optionnelle (le bandeau continue de fonctionner
|
||||||
|
// avec les logos déjà en place même si la migration SQL n'a pas encore été exécutée)
|
||||||
|
if ($category !== null) {
|
||||||
|
$stmt = $dbh->prepare('SELECT * FROM logo_banners WHERE category = :category ORDER BY position ASC, id ASC');
|
||||||
|
$stmt->bindValue(':category', $category, PDO::PARAM_STR);
|
||||||
|
} else {
|
||||||
|
$stmt = $dbh->prepare('SELECT * FROM logo_banners ORDER BY category ASC, position ASC, id ASC');
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui permet de récupérer un logo par id (pour la modification dans le panel Admin)
|
||||||
|
function recupererLogoParId($id) {
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $dbh->prepare('SELECT * FROM logo_banners WHERE id = :id');
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction interne qui affiche le contenu visuel d'une tuile : une icône Devicon si elle est
|
||||||
|
// renseignée (prioritaire), sinon l'image. Ne retourne rien si aucun des deux n'est présent.
|
||||||
|
function afficherLogoVisuel($logo) {
|
||||||
|
$icone = trim($logo['icon'] ?? '');
|
||||||
|
$image = trim($logo['logo_url'] ?? '');
|
||||||
|
$nom = htmlspecialchars($logo['name']);
|
||||||
|
|
||||||
|
if ($icone !== '') {
|
||||||
|
echo '<i class="' . htmlspecialchars($icone) . '"></i>';
|
||||||
|
} elseif ($image !== '') {
|
||||||
|
echo '<img src="' . htmlspecialchars($image) . '" alt="' . $nom . '" loading="lazy">';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui affiche le bandeau défilant d'une catégorie de logos sur la page publique
|
||||||
|
// N'affiche rien si la catégorie est vide (la section entière est masquée côté page d'accueil)
|
||||||
|
function afficherLogos($category) {
|
||||||
|
$logos = listeLogos($category);
|
||||||
|
|
||||||
|
if (empty($logos)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sens = $category === 'partner' ? ' logo-marquee-reverse' : '';
|
||||||
|
|
||||||
|
echo '<div class="logo-marquee' . $sens . '">';
|
||||||
|
echo '<div class="logo-track">';
|
||||||
|
|
||||||
|
// On répète deux fois la liste : le défilement CSS boucle sur -50%, ce qui donne
|
||||||
|
// un effet de scroll infini fluide et sans coupure (voir styles/primary_css.css)
|
||||||
|
for ($repetition = 0; $repetition < 2; $repetition++) {
|
||||||
|
foreach ($logos as $logo) {
|
||||||
|
$nom = htmlspecialchars($logo['name']);
|
||||||
|
$lien = trim($logo['link_url'] ?? '');
|
||||||
|
|
||||||
|
if ($lien !== '') {
|
||||||
|
echo '<a class="logo-tile" href="' . htmlspecialchars($lien) . '" target="_blank" rel="noopener noreferrer" title="' . $nom . '">';
|
||||||
|
} else {
|
||||||
|
echo '<div class="logo-tile" title="' . $nom . '">';
|
||||||
|
}
|
||||||
|
|
||||||
|
afficherLogoVisuel($logo);
|
||||||
|
echo $lien !== '' ? '</a>' : '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui affiche la liste des logos (toutes catégories) avec boutons de
|
||||||
|
// modification / suppression, pour le panel admin
|
||||||
|
function afficherLogosAdmin() {
|
||||||
|
$logos = listeLogos();
|
||||||
|
|
||||||
|
if (empty($logos)) {
|
||||||
|
echo '<p class="admin-notice">Aucun logo pour le moment.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="logo-admin-grid">';
|
||||||
|
|
||||||
|
foreach ($logos as $logo) {
|
||||||
|
$id = (int) $logo['id'];
|
||||||
|
$nom = htmlspecialchars($logo['name']);
|
||||||
|
$badge = $logo['category'] === 'partner' ? 'Entreprise' : 'Outil';
|
||||||
|
|
||||||
|
echo '<div class="logo-admin-item">';
|
||||||
|
echo '<div class="logo-admin-preview">';
|
||||||
|
afficherLogoVisuel($logo);
|
||||||
|
echo '</div>';
|
||||||
|
echo '<div class="logo-admin-info">';
|
||||||
|
echo '<span class="tag">' . $badge . '</span>';
|
||||||
|
echo '<p>' . $nom . '</p>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '<div class="admin-actions">';
|
||||||
|
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_logo=' . $id . '#admin-logos">Modifier</a>';
|
||||||
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeLogo.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce logo ?\');">Supprimer</a>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
@@ -10,13 +10,22 @@ function listeProjets() {
|
|||||||
|
|
||||||
// Essayer
|
// Essayer
|
||||||
try {
|
try {
|
||||||
// Requête SQL qui récupère titre, tags, description, image_url
|
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||||
$stmt = $dbh->prepare('SELECT id, title, tags, description, image_url FROM projects ORDER BY id DESC');
|
// stocké à part dans project_pdfs (voir migration Juillet 2026 (6)), on ne récupère
|
||||||
|
// ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
|
||||||
|
// (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste de projets
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT p.id, p.title, p.status, p.tags, p.description, p.image_url, p.markdown, p.link_url, p.created_at,
|
||||||
|
pp.pdf_filename
|
||||||
|
FROM projects p
|
||||||
|
LEFT JOIN project_pdfs pp ON pp.project_id = p.id
|
||||||
|
ORDER BY p.id DESC
|
||||||
|
');
|
||||||
// Execution de la requête SQL
|
// Execution de la requête SQL
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
// Retourne toutes les données collectés dans un ARRAY
|
// Retourne toutes les données collectés dans un ARRAY
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (PDOException $e) { // En cas d'erreur
|
} catch (PDOException $e) { // En cas d'erreur
|
||||||
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -27,7 +36,15 @@ function recupererProjetParId($id) {
|
|||||||
global $dbh;
|
global $dbh;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $dbh->prepare('SELECT id, title, tags, description, image_url FROM projects WHERE id = :id');
|
// Même principe que listeProjets() : le PDF est joint pour son nom de fichier
|
||||||
|
// uniquement, jamais son contenu binaire
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT p.id, p.title, p.status, p.tags, p.description, p.image_url, p.markdown, p.link_url, p.created_at,
|
||||||
|
pp.pdf_filename
|
||||||
|
FROM projects p
|
||||||
|
LEFT JOIN project_pdfs pp ON pp.project_id = p.id
|
||||||
|
WHERE p.id = :id
|
||||||
|
');
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
@@ -50,9 +67,14 @@ function afficherProjets() {
|
|||||||
// Pour chaque projet dans le ARRAY
|
// Pour chaque projet dans le ARRAY
|
||||||
foreach ($projets as $projet) {
|
foreach ($projets as $projet) {
|
||||||
// On récupère le titre, la description et l'image et le tableau avec les tags
|
// 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']);
|
$titre = htmlspecialchars($projet['title']);
|
||||||
$description = nl2br(htmlspecialchars($projet['description']));
|
$description = nl2br(htmlspecialchars($projet['description']));
|
||||||
$image = htmlspecialchars($projet['image_url']);
|
$image = htmlspecialchars($projet['image_url']);
|
||||||
|
$status = htmlspecialchars($projet['status'] ?? '');
|
||||||
|
$markdown = trim($projet['markdown'] ?? '');
|
||||||
|
$lien = trim($projet['link_url'] ?? '');
|
||||||
|
$pdf = trim($projet['pdf_filename'] ?? '');
|
||||||
$tags = [];
|
$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...)
|
// 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...)
|
||||||
@@ -66,6 +88,11 @@ function afficherProjets() {
|
|||||||
if (!empty($image)) {
|
if (!empty($image)) {
|
||||||
echo '<div class="project-banner">';
|
echo '<div class="project-banner">';
|
||||||
echo '<img src="' . $image . '" alt="Image du projet ' . $titre . '">';
|
echo '<img src="' . $image . '" alt="Image du projet ' . $titre . '">';
|
||||||
|
|
||||||
|
if (!empty($status)) {
|
||||||
|
echo '<div class="project-state">' . $status . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +109,21 @@ function afficherProjets() {
|
|||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajout de la description et fin des classes
|
// Ajout de la description et fin des classes
|
||||||
echo '<p>' . $description . '</p>';
|
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 '</div>';
|
||||||
|
|
||||||
echo '</article>';
|
echo '</article>';
|
||||||
@@ -103,6 +143,7 @@ function afficherProjetsAdmin() {
|
|||||||
$titre = htmlspecialchars($projet['title']);
|
$titre = htmlspecialchars($projet['title']);
|
||||||
$description = nl2br(htmlspecialchars($projet['description']));
|
$description = nl2br(htmlspecialchars($projet['description']));
|
||||||
$image = htmlspecialchars($projet['image_url']);
|
$image = htmlspecialchars($projet['image_url']);
|
||||||
|
$status = htmlspecialchars($projet['status'] ?? '');
|
||||||
$tags = [];
|
$tags = [];
|
||||||
|
|
||||||
if (!empty($projet['tags'])) {
|
if (!empty($projet['tags'])) {
|
||||||
@@ -114,6 +155,11 @@ function afficherProjetsAdmin() {
|
|||||||
if (!empty($image)) {
|
if (!empty($image)) {
|
||||||
echo '<div class="project-banner">';
|
echo '<div class="project-banner">';
|
||||||
echo '<img src="' . $image . '" alt="Image du projet ' . $titre . '">';
|
echo '<img src="' . $image . '" alt="Image du projet ' . $titre . '">';
|
||||||
|
|
||||||
|
if (!empty($status)) {
|
||||||
|
echo '<div class="project-state">' . $status . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,8 +178,8 @@ function afficherProjetsAdmin() {
|
|||||||
|
|
||||||
// Boutons qui vont permettrent la modification / Suppresion
|
// Boutons qui vont permettrent la modification / Suppresion
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '">Modifier</a>';
|
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_project=' . $id . '#admin-projets">Modifier</a>';
|
||||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeProject.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce projet ?\');">Supprimer</a>';
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeProject.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer ce projet ?\');">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Ajout de connexion a la bdd
|
||||||
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
|
// Fonction qui va faire une requête SQL afin de collecter toutes les écoles / formations
|
||||||
|
// Retourne un tableau vide si la table n'existe pas encore (pas de die() pour que la page
|
||||||
|
// Expériences continue de fonctionner tant que le script SQL n'a pas été exécuté)
|
||||||
|
function listeEcoles() {
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||||
|
// stocké à part dans school_pdfs (voir migration Juillet 2026 (6)), on ne récupère
|
||||||
|
// ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu binaire
|
||||||
|
// (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste d'écoles
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT s.id, s.title, s.status, s.tags, s.description, s.duration, s.image_url, s.markdown, s.link_url, s.created_at,
|
||||||
|
sp.pdf_filename
|
||||||
|
FROM schools s
|
||||||
|
LEFT JOIN school_pdfs sp ON sp.school_id = s.id
|
||||||
|
ORDER BY s.id DESC
|
||||||
|
');
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui permet de récupérer une école par id (pour la modification dans le panel Admin)
|
||||||
|
function recupererEcoleParId($id) {
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Même principe que listeEcoles() : le PDF est joint pour son nom de fichier
|
||||||
|
// uniquement, jamais son contenu binaire
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT s.id, s.title, s.status, s.tags, s.description, s.duration, s.image_url, s.markdown, s.link_url, s.created_at,
|
||||||
|
sp.pdf_filename
|
||||||
|
FROM schools s
|
||||||
|
LEFT JOIN school_pdfs sp ON sp.school_id = s.id
|
||||||
|
WHERE s.id = :id
|
||||||
|
');
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction interne qui affiche une carte école (utilisée par la page publique et le panel admin)
|
||||||
|
function afficherEcoleCard($ecole, $admin = false) {
|
||||||
|
$id = (int) $ecole['id'];
|
||||||
|
$titre = htmlspecialchars($ecole['title']);
|
||||||
|
$description = nl2br(htmlspecialchars($ecole['description']));
|
||||||
|
$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...)
|
||||||
|
if (!empty($ecole['tags'])) {
|
||||||
|
$tags = array_filter(array_map('trim', explode(',', $ecole['tags'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<article class="project-card">';
|
||||||
|
|
||||||
|
// Si la donnée avec l'image n'est pas null alors on l'affiche
|
||||||
|
if (!empty($image)) {
|
||||||
|
echo '<div class="project-banner">';
|
||||||
|
echo '<img src="' . $image . '" alt="Image de l ecole ' . $titre . '">';
|
||||||
|
|
||||||
|
if (!empty($status)) {
|
||||||
|
echo '<div class="project-state">' . $status . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="project-content">';
|
||||||
|
echo '<h2>' . $titre . '</h2>';
|
||||||
|
|
||||||
|
echo '<p><strong>Période :</strong> ' . $duration . '</p>';
|
||||||
|
|
||||||
|
if (!empty($tags)) {
|
||||||
|
echo '<div class="project-tags">';
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
echo '<span class="tag">' . htmlspecialchars($tag) . '</span>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 . '#admin-ecoles">Modifier</a>';
|
||||||
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeSchool.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette école ?\');">Supprimer</a>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
echo '</article>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui va afficher les écoles sur la page publique
|
||||||
|
function afficherEcoles() {
|
||||||
|
$ecoles = listeEcoles();
|
||||||
|
|
||||||
|
// Si la table est vide ou n'existe pas encore on n'affiche rien
|
||||||
|
if (empty($ecoles)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="projects-list">';
|
||||||
|
foreach ($ecoles as $ecole) {
|
||||||
|
afficherEcoleCard($ecole, false);
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction identique à celle en haut mais ajout de boutons pour le panel admin
|
||||||
|
function afficherEcolesAdmin() {
|
||||||
|
$ecoles = listeEcoles();
|
||||||
|
|
||||||
|
echo '<div class="projects-list">';
|
||||||
|
foreach ($ecoles as $ecole) {
|
||||||
|
afficherEcoleCard($ecole, true);
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Ajout de connexion a la bdd
|
||||||
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
|
// Fonction qui récupère un réglage dans la table settings (clé / valeur)
|
||||||
|
// Retourne null si le réglage n'existe pas OU si la table n'a pas encore été créée :
|
||||||
|
// contrairement aux autres fonctions on ne fait pas de die() ici pour que le site
|
||||||
|
// continue de fonctionner tant que le script SQL n'a pas été exécuté
|
||||||
|
function recupererSetting($name) {
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $dbh->prepare('SELECT value FROM settings WHERE name = :name');
|
||||||
|
$stmt->bindValue(':name', $name, PDO::PARAM_STR);
|
||||||
|
$stmt->execute();
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $row ? $row['value'] : null;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui affiche la notification de la page d'accueil (si elle est activée)
|
||||||
|
function afficherNotice() {
|
||||||
|
$enabled = recupererSetting('site_notice_enabled');
|
||||||
|
$titre = recupererSetting('site_notice_title');
|
||||||
|
$texte = recupererSetting('site_notice_text');
|
||||||
|
|
||||||
|
// Valeurs par défaut tant que la table settings n'existe pas encore
|
||||||
|
if ($titre === null && $texte === null) {
|
||||||
|
$enabled = '1';
|
||||||
|
$titre = 'Des bugs peuvent encore se produire.';
|
||||||
|
$texte = "Si le CSS ou les images s'affichent mal, pensez à vider le cache de votre navigateur (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la notification est désactivée ou vide on n'affiche rien
|
||||||
|
if ($enabled !== '1' || (empty($titre) && empty($texte))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<p class="dev-cache-notice">';
|
||||||
|
if (!empty($titre)) {
|
||||||
|
echo '<strong>' . htmlspecialchars($titre) . '</strong> ';
|
||||||
|
}
|
||||||
|
echo nl2br(htmlspecialchars($texte));
|
||||||
|
echo '</p>';
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Ajout de connexion a la bdd
|
||||||
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
|
// Fonction qui va faire une requête SQL afin de collecter toutes les catégories de compétences
|
||||||
|
// Retourne un tableau vide si la table n'existe pas encore (pas de die() pour que la page
|
||||||
|
// Compétences continue de fonctionner tant que le script SQL n'a pas été exécuté)
|
||||||
|
function listeSkills() {
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $dbh->prepare('SELECT id, title, icon, items, position FROM skills ORDER BY position ASC, id ASC');
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui permet de récupérer une catégorie de compétences par id (pour la modification dans le panel Admin)
|
||||||
|
function recupererSkillParId($id) {
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $dbh->prepare('SELECT id, title, icon, items, position FROM skills WHERE id = :id');
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icônes Devicon dont la couleur de marque "colored" est noire ou quasi-noire (vérifié dans
|
||||||
|
// devicon.min.css : linux #000, apple #000, bash #293138, ansible #1a1918) : invisibles sur
|
||||||
|
// la tuile sombre du bandeau. On les affiche dans la couleur claire par défaut à la place.
|
||||||
|
const DEVICON_COLORED_TROP_SOMBRE = ['devicon-linux-plain', 'devicon-apple-plain', 'devicon-bash-plain', 'devicon-ansible-plain'];
|
||||||
|
|
||||||
|
// Fonction interne qui affiche le visuel (icône ou image) d'un item de compétence.
|
||||||
|
// Format accepté avant le "|" : soit des classes d'icône (Font Awesome ou Devicon), soit
|
||||||
|
// "img:URL" pour utiliser une vraie image de logo (pour les outils absents des polices
|
||||||
|
// d'icônes, ex : GLPI, FOG, Proxmox, VirtualBox, Guacamole).
|
||||||
|
// $colorise active les vraies couleurs de marque des icônes Devicon (utilisé uniquement
|
||||||
|
// pour le bandeau de la home : sur la page Compétences les icônes restent en orange
|
||||||
|
// uniforme, cohérent avec le reste de la liste)
|
||||||
|
function afficherVisuelItem($itemIcone, $classe = '', $colorise = false) {
|
||||||
|
if (strpos($itemIcone, 'img:') === 0) {
|
||||||
|
$image = htmlspecialchars(substr($itemIcone, 4));
|
||||||
|
echo '<img class="' . $classe . '" src="' . $image . '" alt="" loading="lazy">';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($colorise && strpos($itemIcone, 'devicon-') === 0 && strpos($itemIcone, 'colored') === false) {
|
||||||
|
$estTropSombre = false;
|
||||||
|
foreach (DEVICON_COLORED_TROP_SOMBRE as $slug) {
|
||||||
|
if (strpos($itemIcone, $slug) === 0) {
|
||||||
|
$estTropSombre = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$estTropSombre) {
|
||||||
|
$itemIcone .= ' colored';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<i class="' . $classe . ' ' . htmlspecialchars($itemIcone) . '"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction interne qui affiche une carte de compétences (utilisée par la page publique et le panel admin)
|
||||||
|
// Les items sont stockés un par ligne, au format "classes-icone|Texte" (icône optionnelle)
|
||||||
|
function afficherSkillCard($skill, $admin = false) {
|
||||||
|
$id = (int) $skill['id'];
|
||||||
|
$titre = htmlspecialchars($skill['title']);
|
||||||
|
$icone = htmlspecialchars(!empty($skill['icon']) ? $skill['icon'] : 'fa-solid fa-star');
|
||||||
|
|
||||||
|
echo '<div class="skill-category">';
|
||||||
|
echo '<div class="skill-category-icon"><i class="' . $icone . '"></i></div>';
|
||||||
|
echo '<h2>' . $titre . '</h2>';
|
||||||
|
echo '<ul class="skill-list">';
|
||||||
|
|
||||||
|
// Un item par ligne
|
||||||
|
$items = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $skill['items'])));
|
||||||
|
foreach ($items as $item) {
|
||||||
|
// Icône optionnelle avant le | sinon icône par défaut
|
||||||
|
$itemIcone = 'fa-solid fa-check';
|
||||||
|
$label = $item;
|
||||||
|
if (strpos($item, '|') !== false) {
|
||||||
|
list($itemIcone, $label) = array_map('trim', explode('|', $item, 2));
|
||||||
|
}
|
||||||
|
echo '<li>';
|
||||||
|
afficherVisuelItem($itemIcone, 'skill-item-visual');
|
||||||
|
echo ' ' . htmlspecialchars($label) . '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</ul>';
|
||||||
|
|
||||||
|
// 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 . '#admin-competences">Modifier</a>';
|
||||||
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeSkill.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette catégorie de compétences ?\');">Supprimer</a>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui va afficher les compétences sur la page publique
|
||||||
|
function afficherSkills() {
|
||||||
|
$skills = listeSkills();
|
||||||
|
|
||||||
|
// Si la table est vide ou n'existe pas encore
|
||||||
|
if (empty($skills)) {
|
||||||
|
echo '<p class="admin-notice">Les compétences seront bientôt disponibles.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="skills-grid">';
|
||||||
|
foreach ($skills as $skill) {
|
||||||
|
afficherSkillCard($skill, false);
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction identique à celle en haut mais ajout de boutons pour le panel admin
|
||||||
|
function afficherSkillsAdmin() {
|
||||||
|
$skills = listeSkills();
|
||||||
|
|
||||||
|
echo '<div class="skills-grid">';
|
||||||
|
foreach ($skills as $skill) {
|
||||||
|
afficherSkillCard($skill, true);
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items exclus du bandeau de la home au cas par cas, en plus du filtre automatique
|
||||||
|
// (icône Devicon "réelle" mais jugée pas assez reconnaissable seule, sans le texte à côté)
|
||||||
|
const OUTILS_EXCLUS_DU_BANDEAU = ['Bash'];
|
||||||
|
|
||||||
|
// Fonction qui récupère tous les items de compétences représentant un vrai outil identifiable
|
||||||
|
// (icône Devicon ou image "img:"), toutes catégories confondues. Les items en texte seul
|
||||||
|
// SONT EXCLUS, ainsi que les icônes Font Awesome génériques (fa-solid/fa-brands) : dans un
|
||||||
|
// bandeau qui n'affiche pas le texte à côté (juste une info-bulle au survol), une icône
|
||||||
|
// générique comme un pictogramme "carte" pour "Adressage IP" ou "carnet de contact" pour
|
||||||
|
// "LDAP" ne représente aucun outil reconnaissable — ça reste illisible pour un visiteur qui
|
||||||
|
// ne survole pas. Ces items restent affichés normalement sur la page Compétences (avec leur
|
||||||
|
// texte à côté, donc sans ambiguïté), seul le bandeau de la home les filtre.
|
||||||
|
function listeIconesCompetences() {
|
||||||
|
$categories = listeSkills();
|
||||||
|
$resultats = [];
|
||||||
|
|
||||||
|
foreach ($categories as $categorie) {
|
||||||
|
$items = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $categorie['items'])));
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (strpos($item, '|') === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($icone, $label) = array_map('trim', explode('|', $item, 2));
|
||||||
|
if ($icone === '' || $label === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seuls les vrais logos (Devicon ou image) sont assez reconnaissables sans texte
|
||||||
|
$estUnVraiLogo = strpos($icone, 'devicon-') === 0 || strpos($icone, 'img:') === 0;
|
||||||
|
if (!$estUnVraiLogo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($label, OUTILS_EXCLUS_DU_BANDEAU, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resultats[] = ['icon' => $icone, 'label' => $label];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resultats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction qui affiche le bandeau défilant des outils, directement à partir des icônes
|
||||||
|
// de la page Compétences (voir listeIconesCompetences). Réutilise les mêmes classes CSS
|
||||||
|
// que les bandeaux de logo_banners pour un rendu visuel identique (voir primary_css.css).
|
||||||
|
// "logo-marquee-tools" désactive le filtre grayscale/survol : contrairement aux logos
|
||||||
|
// d'entreprises, on veut que les vrais outils soient reconnaissables immédiatement.
|
||||||
|
function afficherOutilsCompetences() {
|
||||||
|
$outils = listeIconesCompetences();
|
||||||
|
|
||||||
|
if (empty($outils)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="logo-marquee logo-marquee-tools">';
|
||||||
|
echo '<div class="logo-track">';
|
||||||
|
|
||||||
|
// On répète deux fois la liste : le défilement CSS boucle sur -50%, ce qui donne
|
||||||
|
// un effet de scroll infini fluide et sans coupure
|
||||||
|
for ($repetition = 0; $repetition < 2; $repetition++) {
|
||||||
|
foreach ($outils as $outil) {
|
||||||
|
$label = htmlspecialchars($outil['label']);
|
||||||
|
|
||||||
|
echo '<div class="logo-tile" title="' . $label . '">';
|
||||||
|
afficherVisuelItem($outil['icon'], '', true);
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Ajout de connexion a la bdd
|
||||||
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
|
// Fonction qui récupère les chiffres clés affichés sur la home (bandeau de stats)
|
||||||
|
// Chaque compteur retombe sur 0 en cas d'erreur (table pas encore créée...) plutôt
|
||||||
|
// que de bloquer la page d'accueil
|
||||||
|
function recupererStatsHome() {
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
$stats = ['projets' => 0, 'experiences' => 0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stats['projets'] = (int) $dbh->query('SELECT COUNT(*) FROM projects')->fetchColumn();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Table pas encore créée : on laisse le compteur à 0
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stats['experiences'] = (int) $dbh->query('SELECT COUNT(*) FROM experiences')->fetchColumn();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Table pas encore créée : on laisse le compteur à 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ function afficherUsersAdmin()
|
|||||||
echo '<p><strong>Créé le :</strong> ' . htmlspecialchars($user["created_at"]) . '</p>';
|
echo '<p><strong>Créé le :</strong> ' . htmlspecialchars($user["created_at"]) . '</p>';
|
||||||
|
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a href="../scripts/removeUser.php?id=' . $user["id"] . '" class="btn-submit btn-delete" onclick="return confirm(\'Supprimer cet utilisateur ?\')">Supprimer</a>';
|
echo '<a href="../scripts/removeUser.php?id=' . $user["id"] . '&csrf_token=' . urlencode(csrfToken()) . '" class="btn-submit btn-delete" onclick="return confirm(\'Supprimer cet utilisateur ?\')">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|||||||
+62
-9
@@ -13,13 +13,22 @@ function listeXP() {
|
|||||||
|
|
||||||
// Essayer
|
// Essayer
|
||||||
try {
|
try {
|
||||||
// Requête SQL qui récupère titre, type, tags, description, durée, image_url
|
// On liste les colonnes explicitement (pas de SELECT *) : le PDF (pdf_data) est
|
||||||
$stmt = $dbh->prepare('SELECT id, title, xptype, tags, description, duration, image_url FROM experiences ORDER BY id DESC');
|
// stocké à part dans experience_pdfs (voir migration Juillet 2026 (6)), on ne
|
||||||
|
// récupère ici que son nom de fichier via LEFT JOIN, sans jamais charger son contenu
|
||||||
|
// binaire (jusqu'à 15 Mo par PDF) en mémoire pour une simple liste d'expériences
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT e.id, e.title, e.status, e.xptype, e.tags, e.description, e.duration, e.image_url, e.markdown, e.link_url, e.created_at,
|
||||||
|
ep.pdf_filename
|
||||||
|
FROM experiences e
|
||||||
|
LEFT JOIN experience_pdfs ep ON ep.experience_id = e.id
|
||||||
|
ORDER BY e.id DESC
|
||||||
|
');
|
||||||
// Execution de la requête SQL
|
// Execution de la requête SQL
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
// Retourne toutes les données collectés dans un ARRAY
|
// Retourne toutes les données collectés dans un ARRAY
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (PDOException $e) { // En cas d'erreur
|
} catch (PDOException $e) { // En cas d'erreur
|
||||||
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
die("Erreur de connexion à la base de donnée portfolio :".$e->getMessage());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -30,12 +39,20 @@ function recupererXPParId($id) {
|
|||||||
global $dbh;
|
global $dbh;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Préparation de la requête préparée
|
// Même principe que listeXP() : le PDF est joint pour son nom de fichier
|
||||||
$stmt = $dbh->prepare('SELECT id, title, xptype, tags, description, duration, image_url FROM experiences WHERE id = :id');
|
// uniquement, jamais son contenu binaire
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
SELECT e.id, e.title, e.status, e.xptype, e.tags, e.description, e.duration, e.image_url, e.markdown, e.link_url, e.created_at,
|
||||||
|
ep.pdf_filename
|
||||||
|
FROM experiences e
|
||||||
|
LEFT JOIN experience_pdfs ep ON ep.experience_id = e.id
|
||||||
|
WHERE e.id = :id
|
||||||
|
');
|
||||||
|
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
// En cas d'erreur => renvoyer un message avec l'erreur
|
// En cas d'erreur => renvoyer un message avec l'erreur
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur de connexion à la base de donnée portfolio :" . $e->getMessage());
|
die("Erreur de connexion à la base de donnée portfolio :" . $e->getMessage());
|
||||||
// Retourner false en cas d'eerreur
|
// Retourner false en cas d'eerreur
|
||||||
@@ -57,11 +74,16 @@ function afficherXP() {
|
|||||||
// Pour chaque experience dans le ARRAY
|
// Pour chaque experience dans le ARRAY
|
||||||
foreach ($experiences as $experience) {
|
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
|
// 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']);
|
$titre = htmlspecialchars($experience['title']);
|
||||||
$xptype = htmlspecialchars($experience['xptype']);
|
$xptype = htmlspecialchars($experience['xptype']);
|
||||||
$description = nl2br(htmlspecialchars($experience['description']));
|
$description = nl2br(htmlspecialchars($experience['description']));
|
||||||
$duration = htmlspecialchars($experience['duration']);
|
$duration = htmlspecialchars($experience['duration']);
|
||||||
$image = htmlspecialchars($experience['image_url']);
|
$image = htmlspecialchars($experience['image_url']);
|
||||||
|
$status = htmlspecialchars($experience['status'] ?? '');
|
||||||
|
$markdown = trim($experience['markdown'] ?? '');
|
||||||
|
$lien = trim($experience['link_url'] ?? '');
|
||||||
|
$pdf = trim($experience['pdf_filename'] ?? '');
|
||||||
$tags = [];
|
$tags = [];
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +100,11 @@ function afficherXP() {
|
|||||||
if (!empty($image)) {
|
if (!empty($image)) {
|
||||||
echo '<div class="project-banner">';
|
echo '<div class="project-banner">';
|
||||||
echo '<img src="' . $image . '" alt="Image de l experience ' . $titre . '">';
|
echo '<img src="' . $image . '" alt="Image de l experience ' . $titre . '">';
|
||||||
|
|
||||||
|
if (!empty($status)) {
|
||||||
|
echo '<div class="project-state">' . $status . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +112,7 @@ function afficherXP() {
|
|||||||
// Ajout de la class project-content et du titre
|
// Ajout de la class project-content et du titre
|
||||||
echo '<div class="project-content">';
|
echo '<div class="project-content">';
|
||||||
echo '<h2>' . $titre . '</h2>';
|
echo '<h2>' . $titre . '</h2>';
|
||||||
|
|
||||||
echo '<p><strong>Type :</strong> ' . $xptype . '</p>';
|
echo '<p><strong>Type :</strong> ' . $xptype . '</p>';
|
||||||
echo '<p><strong>Durée :</strong> ' . $duration . '</p>';
|
echo '<p><strong>Durée :</strong> ' . $duration . '</p>';
|
||||||
|
|
||||||
@@ -99,8 +127,21 @@ function afficherXP() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Ajout de la description et fin des classes
|
// Ajout de la description et fin des classes
|
||||||
echo '<p>' . $description . '</p>';
|
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>';
|
echo '</div>';
|
||||||
|
|
||||||
|
|
||||||
@@ -123,6 +164,7 @@ function afficherXPAdmin() {
|
|||||||
$xptype = htmlspecialchars($experience['xptype']);
|
$xptype = htmlspecialchars($experience['xptype']);
|
||||||
$description = nl2br(htmlspecialchars($experience['description']));
|
$description = nl2br(htmlspecialchars($experience['description']));
|
||||||
$duration = htmlspecialchars($experience['duration']);
|
$duration = htmlspecialchars($experience['duration']);
|
||||||
|
$status = htmlspecialchars($experience['status'] ?? '');
|
||||||
$image = htmlspecialchars($experience['image_url']);
|
$image = htmlspecialchars($experience['image_url']);
|
||||||
$tags = [];
|
$tags = [];
|
||||||
|
|
||||||
@@ -135,11 +177,22 @@ function afficherXPAdmin() {
|
|||||||
if (!empty($image)) {
|
if (!empty($image)) {
|
||||||
echo '<div class="project-banner">';
|
echo '<div class="project-banner">';
|
||||||
echo '<img src="' . $image . '" alt="Image de l experience ' . $titre . '">';
|
echo '<img src="' . $image . '" alt="Image de l experience ' . $titre . '">';
|
||||||
|
|
||||||
|
if (!empty($status)) {
|
||||||
|
echo '<div class="project-state">' . $status . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
echo '<div class="project-content">';
|
echo '<div class="project-content">';
|
||||||
echo '<h2>' . $titre . '</h2>';
|
echo '<h2>' . $titre . '</h2>';
|
||||||
|
|
||||||
|
// Si le status n'est pas vide alors on l'affiche
|
||||||
|
if (!empty($status)) {
|
||||||
|
echo '<div class="project-state">' . $status . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
echo '<p><strong>Type :</strong> ' . $xptype . '</p>';
|
echo '<p><strong>Type :</strong> ' . $xptype . '</p>';
|
||||||
echo '<p><strong>Durée :</strong> ' . $duration . '</p>';
|
echo '<p><strong>Durée :</strong> ' . $duration . '</p>';
|
||||||
|
|
||||||
@@ -155,8 +208,8 @@ function afficherXPAdmin() {
|
|||||||
|
|
||||||
// Boutons qui vont permettrent la modification / Suppresion
|
// Boutons qui vont permettrent la modification / Suppresion
|
||||||
echo '<div class="admin-actions">';
|
echo '<div class="admin-actions">';
|
||||||
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '">Modifier</a>';
|
echo '<a class="btn-submit btn-edit" href="./admin.php?edit_xp=' . $id . '#admin-experiences">Modifier</a>';
|
||||||
echo '<a class="btn-submit btn-delete" href="../scripts/removeXP.php?id=' . $id . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette expérience ?\');">Supprimer</a>';
|
echo '<a class="btn-submit btn-delete" href="../scripts/removeXP.php?id=' . $id . '&csrf_token=' . urlencode(csrfToken()) . '" onclick="return confirm(\'Voulez-vous vraiment supprimer cette expérience ?\');">Supprimer</a>';
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|||||||
@@ -0,0 +1,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
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
// Sécurité transverse : cookie de session durci, en-têtes anti-clickjacking et
|
||||||
|
// protection CSRF. A inclure à la place d'un simple session_start() sur toute
|
||||||
|
// page ou script qui utilise une session (remplace l'appel direct partout).
|
||||||
|
|
||||||
|
// Règle les paramètres du cookie de session AVANT de démarrer la session (ils
|
||||||
|
// n'ont aucun effet une fois la session déjà démarrée) :
|
||||||
|
// - httponly : empêche le JavaScript de lire le cookie (limite le vol de session via une XSS)
|
||||||
|
// - samesite=Lax : bloque une partie des requêtes intersites (CSRF)
|
||||||
|
// - secure : n'est activé que si la connexion est déjà en HTTPS (sinon le cookie ne partirait jamais en HTTP)
|
||||||
|
function configureSecureSessionCookies() {
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anti-clickjacking : interdit d'afficher le site dans une <iframe> d'un autre site
|
||||||
|
header('X-Frame-Options: SAMEORIGIN');
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
configureSecureSessionCookies();
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génère (si besoin) et retourne le jeton CSRF de la session en cours, à
|
||||||
|
// placer dans un champ caché de chaque formulaire admin ou dans les liens
|
||||||
|
// GET qui déclenchent une action (suppression, export...)
|
||||||
|
function csrfToken() {
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifie le jeton CSRF reçu (POST ou GET) ; arrête le script si invalide.
|
||||||
|
// A appeler juste après la vérification du rôle admin dans chaque script d'action.
|
||||||
|
function requireCsrfToken() {
|
||||||
|
$recu = $_POST['csrf_token'] ?? $_GET['csrf_token'] ?? '';
|
||||||
|
if (empty($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $recu)) {
|
||||||
|
http_response_code(403);
|
||||||
|
die("Requête refusée : jeton de sécurité invalide ou expiré. Recharge la page et réessaie.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-4
@@ -1,15 +1,36 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// On cache les webhooks discord => Correction de faille de sécurité
|
||||||
|
// __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
|
// Fonction qui va ENVOYER des logs sur le webhook discord
|
||||||
function sendLog($action, $user="") {
|
function sendLog($action, $user = "") {
|
||||||
$webhook = "https://discord.com/api/webhooks/1513460601056264242/JzPzMzbxWSGDzL-5Gfqa6tkzzaW74ZV9026-nSraOHS_dQF_YD1-N4S6M6IUv_kKoNhn";
|
// 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 = "**Nouvelle log du portfolio**\n\n";
|
||||||
$contenuLog .= "**Action :** " . $action . "\n";
|
$contenuLog .= "**Action :** " . $action . "\n";
|
||||||
if ($user != "") {
|
if ($user != "") {
|
||||||
$contenuLog .= "**Utilisateur :** " . $user . "\n";
|
$contenuLog .= "**Utilisateur :** " . $user . "\n";
|
||||||
} else {
|
} 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 = [
|
$data = [
|
||||||
"content" => $contenuLog
|
"content" => $contenuLog
|
||||||
@@ -22,4 +43,36 @@ function sendLog($action, $user="") {
|
|||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataJson);
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataJson);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
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,12 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
// Ce projet est entièrement développé par Esteban MOUSSU
|
// 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
|
// 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...)
|
||||||
|
|
||||||
// Note : 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
|
|
||||||
|
|
||||||
// Le header n'y étant pas je rajoute ici un démarrage de session
|
// Le header n'y étant pas je rajoute ici un démarrage de session
|
||||||
session_start();
|
require_once(__DIR__ . '/functions/security.php');
|
||||||
|
|
||||||
// Redirection de l'utilisateur vers une page Home lorsqu'il arrive sur index.php
|
// Redirection de l'utilisateur vers une page Home lorsqu'il arrive sur index.php
|
||||||
header('location: ./pages/home.php');
|
header('location: ./pages/home.php');
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+550
-91
@@ -1,4 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// Titre de l'onglet
|
||||||
|
$pageTitle = 'Administration';
|
||||||
|
|
||||||
// On ajoute ici le header
|
// On ajoute ici le header
|
||||||
require_once('./../elements/header.php');
|
require_once('./../elements/header.php');
|
||||||
|
|
||||||
@@ -7,6 +10,10 @@ require_once '../functions/databaseConnection.php';
|
|||||||
require_once("../functions/listUser.php");
|
require_once("../functions/listUser.php");
|
||||||
require_once('../functions/listProject.php');
|
require_once('../functions/listProject.php');
|
||||||
require_once('../functions/listXP.php');
|
require_once('../functions/listXP.php');
|
||||||
|
require_once('../functions/listSchool.php');
|
||||||
|
require_once('../functions/listSkills.php');
|
||||||
|
require_once('../functions/listSettings.php');
|
||||||
|
require_once('../functions/listLogos.php');
|
||||||
|
|
||||||
|
|
||||||
// L'utilisateur doit être administrateur
|
// L'utilisateur doit être administrateur
|
||||||
@@ -18,74 +25,159 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
// On met les variables à null ici pour éviter qu'il garde les options en premier chargement
|
// On met les variables à null ici pour éviter qu'il garde les options en premier chargement
|
||||||
$projetAModifier = null;
|
$projetAModifier = null;
|
||||||
$xpAModifier = null;
|
$xpAModifier = null;
|
||||||
|
$ecoleAModifier = null;
|
||||||
|
$skillAModifier = null;
|
||||||
|
$logoAModifier = null;
|
||||||
|
|
||||||
|
|
||||||
// Si on modifie XP ou Projets
|
// Si on modifie XP, Projets, École ou Compétence
|
||||||
if (isset($_GET['edit_project']) && is_numeric($_GET['edit_project'])) {
|
if (isset($_GET['edit_project']) && is_numeric($_GET['edit_project'])) {
|
||||||
$projetAModifier = recupererProjetParId((int) $_GET['edit_project']);
|
$projetAModifier = recupererProjetParId((int) $_GET['edit_project']);
|
||||||
}
|
}
|
||||||
if (isset($_GET['edit_xp']) && is_numeric($_GET['edit_xp'])) {
|
if (isset($_GET['edit_xp']) && is_numeric($_GET['edit_xp'])) {
|
||||||
$xpAModifier = recupererXPParId((int) $_GET['edit_xp']);
|
$xpAModifier = recupererXPParId((int) $_GET['edit_xp']);
|
||||||
}
|
}
|
||||||
|
if (isset($_GET['edit_school']) && is_numeric($_GET['edit_school'])) {
|
||||||
|
$ecoleAModifier = recupererEcoleParId((int) $_GET['edit_school']);
|
||||||
|
}
|
||||||
|
if (isset($_GET['edit_skill']) && is_numeric($_GET['edit_skill'])) {
|
||||||
|
$skillAModifier = recupererSkillParId((int) $_GET['edit_skill']);
|
||||||
|
}
|
||||||
|
if (isset($_GET['edit_logo']) && is_numeric($_GET['edit_logo'])) {
|
||||||
|
$logoAModifier = recupererLogoParId((int) $_GET['edit_logo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valeurs actuelles de la notification de la page d'accueil
|
||||||
|
$noticeTitle = recupererSetting('site_notice_title') ?? '';
|
||||||
|
$noticeText = recupererSetting('site_notice_text') ?? '';
|
||||||
|
$noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||||
|
|
||||||
|
// Valeur actuelle du captcha anti-robot (activé par défaut)
|
||||||
|
$captchaEnabled = recupererSetting('captcha_enabled') ?? '1';
|
||||||
|
|
||||||
|
// Valeurs actuelles des coordonnées de la page contact
|
||||||
|
$contactEmail = recupererSetting('contact_email') ?? 'esteban.moussu@ik.me';
|
||||||
|
$contactEmailEnabled = recupererSetting('contact_email_enabled') ?? '1';
|
||||||
|
$contactAddressLine1 = recupererSetting('contact_address_line1') ?? '20 Grande rue';
|
||||||
|
$contactAddressLine2 = recupererSetting('contact_address_line2') ?? '52500 Broncourt';
|
||||||
|
$contactAddressEnabled = recupererSetting('contact_address_enabled') ?? '1';
|
||||||
|
|
||||||
// Page du panneau d'administration
|
// Page du panneau d'administration
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="admin-forms">
|
<div class="admin-page">
|
||||||
|
|
||||||
<!-- Formulaire de création de projet -->
|
<!-- Titre du panneau -->
|
||||||
<div class="project-container">
|
<div class="page-header">
|
||||||
<div class="project-form-card">
|
<h1>Panneau d'administration</h1>
|
||||||
|
<p>Gère le contenu du site : projets, expériences, écoles, compétences, logos, notification et utilisateurs.</p>
|
||||||
<div class="project-header">
|
|
||||||
<h2><?php echo $projetAModifier ? 'Modifier le projet' : 'Nouveau projet'; ?></h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- en envoie les données à addProject.php -->
|
|
||||||
<form method="POST" action="<?php echo $projetAModifier ? '../scripts/updateProject.php' : '../scripts/addProject.php'; ?>">
|
|
||||||
|
|
||||||
<?php if ($projetAModifier): ?>
|
|
||||||
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<label>Titre *</label>
|
|
||||||
<input type="text" name="title" required value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['title']) : ''; ?>">
|
|
||||||
|
|
||||||
<label>Tags (virgules)</label>
|
|
||||||
<input type="text" name="tags" value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['tags']) : ''; ?>">
|
|
||||||
|
|
||||||
<label>Description *</label>
|
|
||||||
<textarea name="description" required><?php echo $projetAModifier ? htmlspecialchars($projetAModifier['description']) : ''; ?></textarea>
|
|
||||||
|
|
||||||
<label>Lien de l'image *</label>
|
|
||||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $projetAModifier ? htmlspecialchars($projetAModifier['image_url']) : ''; ?>">
|
|
||||||
|
|
||||||
<input type="submit" name="submitproject" value="<?php echo $projetAModifier ? 'Modifier le projet' : '+ Ajouter le projet'; ?>" class="btn-submit">
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Formulaire de création d'expérience -->
|
<!-- Sous-navigation : accès rapide à chaque catégorie -->
|
||||||
<div class="project-container">
|
<nav class="admin-nav">
|
||||||
<div class="project-form-card">
|
<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>
|
||||||
<div class="project-header">
|
<a href="#admin-ecoles"><i class="fa-solid fa-graduation-cap"></i> Écoles</a>
|
||||||
<h2><?php echo $xpAModifier ? 'Modifier l\'experience' : 'Nouvelle expérience'; ?></h2>
|
<a href="#admin-competences"><i class="fa-solid fa-code"></i> Compétences</a>
|
||||||
</div>
|
<a href="#admin-logos"><i class="fa-solid fa-images"></i> Logos</a>
|
||||||
|
<a href="#admin-contact"><i class="fa-solid fa-address-card"></i> Contact</a>
|
||||||
|
<a href="#admin-notification"><i class="fa-solid fa-bell"></i> Notification</a>
|
||||||
|
<a href="#admin-securite"><i class="fa-solid fa-shield-halved"></i> Sécurité</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>
|
||||||
|
|
||||||
<!-- en envoie les données à addXP.php -->
|
<!-- ========== SECTION PROJETS ========== -->
|
||||||
<form method="POST" action="<?php echo $xpAModifier ? '../scripts/updateXP.php' : '../scripts/addXP.php'; ?>">
|
<section id="admin-projets" class="admin-section">
|
||||||
|
<h2 class="admin-section-title"><i class="fa-solid fa-folder-open"></i> Projets</h2>
|
||||||
<?php if ($xpAModifier): ?>
|
|
||||||
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
|
<!-- Formulaire (replié par défaut, ouvert automatiquement en mode modification) -->
|
||||||
<?php endif; ?>
|
<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" enctype="multipart/form-data" action="<?php echo $projetAModifier ? '../scripts/updateProject.php' : '../scripts/addProject.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
|
<?php if ($projetAModifier): ?>
|
||||||
|
<input type="hidden" name="id" value="<?php echo (int) $projetAModifier['id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<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']) : ''; ?>">
|
||||||
|
|
||||||
|
<label>Description *</label>
|
||||||
|
<textarea name="description" required><?php echo $projetAModifier ? htmlspecialchars($projetAModifier['description']) : ''; ?></textarea>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div class="project-form-card">
|
||||||
|
<!-- en envoie les données à addXP.php -->
|
||||||
|
<form method="POST" enctype="multipart/form-data" action="<?php echo $xpAModifier ? '../scripts/updateXP.php' : '../scripts/addXP.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
|
<?php if ($xpAModifier): ?>
|
||||||
|
<input type="hidden" name="id" value="<?php echo (int) $xpAModifier['id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<label>Titre *</label>
|
<label>Titre *</label>
|
||||||
<input type="text" name="title" required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['title']) : ''; ?>">
|
<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>
|
<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>
|
<label>Tags (virgules)</label>
|
||||||
<input type="text" name="tags" value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['tags']) : ''; ?>">
|
<input type="text" name="tags" value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['tags']) : ''; ?>">
|
||||||
@@ -94,57 +186,424 @@ if (isset($_GET['edit_xp']) && is_numeric($_GET['edit_xp'])) {
|
|||||||
<textarea name="description" required><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['description']) : ''; ?></textarea>
|
<textarea name="description" required><?php echo $xpAModifier ? htmlspecialchars($xpAModifier['description']) : ''; ?></textarea>
|
||||||
|
|
||||||
<label>Durée *</label>
|
<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>
|
<label>Lien de l'image *</label>
|
||||||
<input type="url" name="imageurl" placeholder="https://..." required value="<?php echo $xpAModifier ? htmlspecialchars($xpAModifier['image_url']) : ''; ?>">
|
<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">
|
<input type="submit" name="submitxp" value="<?php echo $xpAModifier ? 'Modifier l\'experience' : '+ Ajouter l\'experience'; ?>" class="btn-submit">
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div class="project-form-card">
|
||||||
|
<!-- on envoie les données à addSchool.php -->
|
||||||
|
<form method="POST" enctype="multipart/form-data" action="<?php echo $ecoleAModifier ? '../scripts/updateSchool.php' : '../scripts/addSchool.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
|
<?php if ($ecoleAModifier): ?>
|
||||||
|
<input type="hidden" name="id" value="<?php echo (int) $ecoleAModifier['id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<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']) : ''; ?>">
|
||||||
|
|
||||||
|
<label>Description (diplôme, spécialité...) *</label>
|
||||||
|
<textarea name="description" required><?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['description']) : ''; ?></textarea>
|
||||||
|
|
||||||
|
<label>Période (ex : 2023 - 2025) *</label>
|
||||||
|
<input type="text" name="duration" required value="<?php echo $ecoleAModifier ? htmlspecialchars($ecoleAModifier['duration']) : ''; ?>">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div class="project-form-card">
|
||||||
|
<!-- on envoie les données à addSkill.php -->
|
||||||
|
<form method="POST" action="<?php echo $skillAModifier ? '../scripts/updateSkill.php' : '../scripts/addSkill.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
|
<?php if ($skillAModifier): ?>
|
||||||
|
<input type="hidden" name="id" value="<?php echo (int) $skillAModifier['id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label>Titre de la catégorie *</label>
|
||||||
|
<input type="text" name="title" required value="<?php echo $skillAModifier ? htmlspecialchars($skillAModifier['title']) : ''; ?>">
|
||||||
|
|
||||||
|
<label>Icône de la catégorie (classes Font Awesome, ex : fa-solid fa-terminal)</label>
|
||||||
|
<input type="text" name="icon" placeholder="fa-solid fa-star" value="<?php echo $skillAModifier ? htmlspecialchars($skillAModifier['icon']) : ''; ?>">
|
||||||
|
|
||||||
|
<label>Compétences (une par ligne, format : "icone|texte" ou juste "texte") *</label>
|
||||||
|
<textarea name="items" placeholder="fa-brands fa-python|Python fa-solid fa-terminal|Bash Ansible" required><?php echo $skillAModifier ? htmlspecialchars($skillAModifier['items']) : ''; ?></textarea>
|
||||||
|
|
||||||
|
<label>Position (ordre d'affichage, du plus petit au plus grand)</label>
|
||||||
|
<input type="text" name="position" value="<?php echo $skillAModifier ? (int) $skillAModifier['position'] : '0'; ?>">
|
||||||
|
|
||||||
|
<input type="submit" name="submitskill" value="<?php echo $skillAModifier ? 'Modifier la compétence' : '+ Ajouter la compétence'; ?>" class="btn-submit">
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Liste des compétences avec modification / suppression -->
|
||||||
|
<div class="skills-section skills-section-admin">
|
||||||
|
<?php afficherSkillsAdmin(); ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
|
<!-- ========== SECTION LOGOS (bandeaux page d'accueil) ========== -->
|
||||||
|
<section id="admin-logos" class="admin-section">
|
||||||
|
<h2 class="admin-section-title"><i class="fa-solid fa-images"></i> Logos partenaires (page d'accueil)</h2>
|
||||||
|
<p class="admin-data-text admin-section-intro">
|
||||||
|
Le bandeau "Outils que je sais manipuler" est désormais dérivé automatiquement de la
|
||||||
|
page <a href="#admin-competences">Compétences</a> (icône dans le champ "icone|texte" de chaque item).
|
||||||
|
Cette section ne gère plus que le bandeau "Ils m'ont fait confiance".
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details class="admin-block" <?php echo $logoAModifier ? 'open' : ''; ?>>
|
||||||
|
<summary><?php echo $logoAModifier ? 'Modifier le logo : ' . htmlspecialchars($logoAModifier['name']) : '+ Nouvelle entreprise'; ?></summary>
|
||||||
|
|
||||||
|
<div class="project-form-card">
|
||||||
|
<!-- on envoie les données à addLogo.php -->
|
||||||
|
<form method="POST" action="<?php echo $logoAModifier ? '../scripts/updateLogo.php' : '../scripts/addLogo.php'; ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
|
||||||
|
<?php if ($logoAModifier): ?>
|
||||||
|
<input type="hidden" name="id" value="<?php echo (int) $logoAModifier['id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Seul le bandeau "Ils m'ont fait confiance" est géré ici -->
|
||||||
|
<input type="hidden" name="category" value="partner">
|
||||||
|
|
||||||
|
<label>Nom de l'entreprise *</label>
|
||||||
|
<input type="text" name="name" required placeholder="Nom de l'entreprise..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['name']) : ''; ?>">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Icône Devicon (facultatif, rarement utile pour une entreprise — parcourir sur
|
||||||
|
<a href="https://devicon.dev" target="_blank" rel="noopener noreferrer">devicon.dev</a>)
|
||||||
|
</label>
|
||||||
|
<input type="text" name="icon" placeholder="devicon-docker-plain colored" value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['icon'] ?? '') : ''; ?>">
|
||||||
|
|
||||||
|
<label>Lien de l'image (utilisé si pas d'icône)</label>
|
||||||
|
<input type="url" name="logourl" placeholder="https://..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['logo_url'] ?? '') : ''; ?>">
|
||||||
|
|
||||||
|
<label>Lien externe (optionnel, rend le logo cliquable)</label>
|
||||||
|
<input type="url" name="linkurl" placeholder="https://..." value="<?php echo $logoAModifier ? htmlspecialchars($logoAModifier['link_url'] ?? '') : ''; ?>">
|
||||||
|
|
||||||
|
<label>Position (ordre d'affichage, du plus petit au plus grand)</label>
|
||||||
|
<input type="text" name="position" value="<?php echo $logoAModifier ? (int) $logoAModifier['position'] : '0'; ?>">
|
||||||
|
|
||||||
|
<input type="submit" name="submitlogo" value="<?php echo $logoAModifier ? 'Modifier le logo' : '+ Ajouter le logo'; ?>" class="btn-submit">
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Liste des logos avec modification / suppression -->
|
||||||
|
<?php afficherLogosAdmin(); ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ========== SECTION CONTACT ========== -->
|
||||||
|
<section id="admin-contact" class="admin-section">
|
||||||
|
<h2 class="admin-section-title"><i class="fa-solid fa-address-card"></i> Coordonnées de contact</h2>
|
||||||
|
|
||||||
|
<div class="project-container">
|
||||||
|
<div class="project-form-card">
|
||||||
|
<!-- on envoie les données à updateSettings.php -->
|
||||||
|
<form method="POST" action="../scripts/updateSettings.php">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
<input type="hidden" name="settings_group" value="contact">
|
||||||
|
|
||||||
|
<label>Adresse email</label>
|
||||||
|
<input type="email" name="contact_email" value="<?php echo htmlspecialchars($contactEmail); ?>">
|
||||||
|
|
||||||
|
<!-- Checkbox pour afficher / masquer l'email sur la page contact -->
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" name="contact_email_enabled" value="1" <?php echo $contactEmailEnabled === '1' ? 'checked' : ''; ?>>
|
||||||
|
Afficher l'email sur la page contact
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>Adresse postale - ligne 1 (rue)</label>
|
||||||
|
<input type="text" name="contact_address_line1" value="<?php echo htmlspecialchars($contactAddressLine1); ?>">
|
||||||
|
|
||||||
|
<label>Adresse postale - ligne 2 (code postal / ville)</label>
|
||||||
|
<input type="text" name="contact_address_line2" value="<?php echo htmlspecialchars($contactAddressLine2); ?>">
|
||||||
|
|
||||||
|
<!-- Checkbox pour afficher / masquer l'adresse postale sur la page contact -->
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" name="contact_address_enabled" value="1" <?php echo $contactAddressEnabled === '1' ? 'checked' : ''; ?>>
|
||||||
|
Afficher l'adresse postale sur la page contact
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="submit" name="submitsettings" value="Enregistrer les coordonnées" class="btn-submit">
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div class="project-container">
|
||||||
|
<div class="project-form-card">
|
||||||
|
<!-- on envoie les données à updateSettings.php -->
|
||||||
|
<form method="POST" action="../scripts/updateSettings.php">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
<input type="hidden" name="settings_group" value="notice">
|
||||||
|
|
||||||
|
<label>Titre (affiché en gras)</label>
|
||||||
|
<input type="text" name="notice_title" value="<?php echo htmlspecialchars($noticeTitle); ?>">
|
||||||
|
|
||||||
|
<label>Texte</label>
|
||||||
|
<textarea name="notice_text"><?php echo htmlspecialchars($noticeText); ?></textarea>
|
||||||
|
|
||||||
|
<!-- Checkbox pour activer / désactiver la notification sans la supprimer -->
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" name="notice_enabled" value="1" <?php echo $noticeEnabled === '1' ? 'checked' : ''; ?>>
|
||||||
|
Afficher la notification sur la page d'accueil
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="submit" name="submitsettings" value="Enregistrer la notification" class="btn-submit">
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ========== SECTION SÉCURITÉ ========== -->
|
||||||
|
<section id="admin-securite" class="admin-section">
|
||||||
|
<h2 class="admin-section-title"><i class="fa-solid fa-shield-halved"></i> Sécurité</h2>
|
||||||
|
|
||||||
|
<div class="project-container">
|
||||||
|
<div class="project-form-card">
|
||||||
|
<!-- on envoie les données à updateSettings.php -->
|
||||||
|
<form method="POST" action="../scripts/updateSettings.php">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
<input type="hidden" name="settings_group" value="captcha">
|
||||||
|
|
||||||
|
<!-- Checkbox pour activer / désactiver le test anti-robot à l'entrée du site -->
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" name="captcha_enabled" value="1" <?php echo $captchaEnabled === '1' ? 'checked' : ''; ?>>
|
||||||
|
Activer le test anti-robot (captcha) à l'entrée du site
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="submit" name="submitsettings" value="Enregistrer" class="btn-submit">
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ========== SECTION UTILISATEURS ========== -->
|
||||||
|
<section id="admin-utilisateurs" class="admin-section">
|
||||||
|
<h2 class="admin-section-title"><i class="fa-solid fa-users"></i> Utilisateurs</h2>
|
||||||
|
|
||||||
|
<details class="admin-block">
|
||||||
|
<summary>+ Nouveau compte</summary>
|
||||||
|
|
||||||
|
<div class="project-form-card">
|
||||||
|
<!-- en envoie les données addUser.php -->
|
||||||
|
<form method="POST" action="../scripts/addUser.php">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
<label>Nom du compte</label>
|
||||||
|
<input type="text" name="user" required>
|
||||||
|
|
||||||
|
<!-- role admin ou user -->
|
||||||
|
<label>Rôle (admin/user)</label>
|
||||||
|
<input type="text" name="role" placeholder="user" required>
|
||||||
|
|
||||||
|
<!-- mot de passe -->
|
||||||
|
<label>Mot de passe</label>
|
||||||
|
<input type="password" name="passwd" required>
|
||||||
|
|
||||||
|
<!-- Le bouton qui déclenche le PHP en haut de page -->
|
||||||
|
<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?csrf_token=<?php echo urlencode(csrfToken()); ?>">
|
||||||
|
<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="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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
|
||||||
|
// Garde-fou : input.click() déclenche lui-même un évènement "click" qui remonte
|
||||||
|
// (bubble) jusqu'à la zone puisque l'input est à l'intérieur de celle-ci. Sans ce
|
||||||
|
// test, ce clic remonté relance input.click() une seconde fois => double sélecteur.
|
||||||
|
zone.addEventListener('click', function (e) {
|
||||||
|
if (e.target === input) return;
|
||||||
|
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>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Liste des projets / xps => en gros on pourra supprimer, et modifier à la vollée sans aller dans la bdd
|
// On ajoute ici le footer
|
||||||
?>
|
require_once('../elements/footer.php'); ?>
|
||||||
<h2 class="projects-title">Gérer les projets</h2>
|
|
||||||
<?php afficherProjetsAdmin(); ?>
|
|
||||||
<h2 class="projects-title">Gérer les experiences</h2>
|
|
||||||
<?php afficherXPAdmin();
|
|
||||||
|
|
||||||
// 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>
|
|
||||||
<input type="text" name="user" required>
|
|
||||||
|
|
||||||
<!-- role admin ou user -->
|
|
||||||
<label>Rôle (admin/user)</label>
|
|
||||||
<textarea name="role" required></textarea>
|
|
||||||
|
|
||||||
<label>Mot de passe</label>
|
|
||||||
<textarea name="passwd" required></textarea>
|
|
||||||
|
|
||||||
<!-- Le bouton qui déclenche le PHP en haut de page -->
|
|
||||||
<input type="submit" name="submituser" value="Ajouter l'utilisateur" class="btn-submit">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="projects-title">Gérer les utilisateurs</h2>
|
|
||||||
|
|
||||||
<?php afficherUsersAdmin();
|
|
||||||
|
|
||||||
require_once('../elements/footer.php'); ?>
|
|
||||||
|
|||||||
+62
-3
@@ -2,8 +2,67 @@
|
|||||||
|
|
||||||
// CGU / CDU
|
// CGU / CDU
|
||||||
|
|
||||||
// On ajoute ici le header
|
// Titre de l'onglet
|
||||||
|
$pageTitle = 'Conditions générales d\'utilisation';
|
||||||
|
|
||||||
require_once('./../elements/header.php');
|
require_once('./../elements/header.php');
|
||||||
|
|
||||||
// Ajout du footer
|
?>
|
||||||
require_once('../elements/footer.php'); ?>
|
|
||||||
|
<div class="cgu-page">
|
||||||
|
<section class="cgu-card">
|
||||||
|
<h1> Conditions générales d'utilisation </h1>
|
||||||
|
<h2> Dernière mise à jour : 29 juin 2026</h2>
|
||||||
|
<hr>
|
||||||
|
<p>Les présentes conditions générales d'utilisation (CGU) régissent l'utilisation du site web estebanadmin.fr. En accédant à ce site, vous acceptez les présentes conditions.</p>
|
||||||
|
|
||||||
|
<h3> 1. Objet</h3>
|
||||||
|
<p>Le présent site a pour objectif de présenter le parcours, les compétences, les projets et les réalisations d’Esteban. Le site peut également contenir des moyens de contact, des liens externes et des contenus liés à son activité personnelle, associative ou professionnelle.</p>
|
||||||
|
|
||||||
|
<h3> 2. Accès au site</h3>
|
||||||
|
<p>L'accès au site est gratuit et ouvert à tous. L'utilisateur s'engage à utiliser le site de manière responsable et conforme à la législation en vigueur.</p>
|
||||||
|
<p>L'éditeur se réserve le droit de suspendre, d'interrompre ou de modifier tout ou partie du site sans préavis, notamment pour des raisons techniques, légales, de sécurité, de maintenance ou en cas d'usage abusif.</p>
|
||||||
|
|
||||||
|
<h3> 3. Propriété intellectuelle</h3>
|
||||||
|
<p>L’ensemble des éléments présents sur le site, notamment les textes, visuels, illustrations, logos, éléments graphiques, structure, code source et contenus originaux, est protégé par le droit de la propriété intellectuelle, sauf mention contraire.</p>
|
||||||
|
<p>Toute reproduction, représentation, diffusion, adaptation, modification ou exploitation, totale ou partielle, sans autorisation préalable écrite, est interdite, sauf dans les cas autorisés par la loi.</p>
|
||||||
|
|
||||||
|
<h3> 4. Utilisation de l’intelligence artificielle</h3>
|
||||||
|
<p>Le site et certains de ses éléments ont pu être conçus avec l'assistance d'outils d’intelligence artificielle. Cela peut notamment concerner la génération ou l’adaptation de feuilles de style CSS, l’aide à la rédaction, l’autocomplétion de code, la génération de commentaires, de structures répétitives, de formulaires simples ou de requêtes basiques.</p>
|
||||||
|
<p>L’éditeur précise cependant que le développement principal du projet, notamment le code PHP, HTML et JavaScript, l’architecture générale, l’intégration, les choix techniques et la personnalisation globale du site, ont été réalisés majoritairement par un humain, à savoir Esteban MOUSSU.</p>
|
||||||
|
<p>L'utilisation d'outils tels que GitHub Copilot, Devin ou d'autres assistants similaires constitue une aide au développement et non une substitution complète au travail humain.</p>
|
||||||
|
|
||||||
|
<h3> 5. Hébergement et édition</h3>
|
||||||
|
<p>Ce projet est entièrement développé par Esteban MOUSSU.</p>
|
||||||
|
<p>Le site est hébergé en 2026 par l’entreprise TerraTech Hebergement, en collaboration avec l’association Eldoria Network.</p>
|
||||||
|
<p>Les informations diffusées sur le site le sont sous la responsabilité de son éditeur.</p>
|
||||||
|
|
||||||
|
<h3> 6. Exactitude des informations</h3>
|
||||||
|
<p>L’éditeur s’efforce de fournir des informations aussi exactes et à jour que possible. Toutefois, aucune garantie n’est donnée quant à l’exactitude, l’exhaustivité ou l’actualité des contenus publiés.</p>
|
||||||
|
<p>L’utilisateur reconnaît utiliser les informations disponibles sur le site sous sa seule responsabilité.</p>
|
||||||
|
|
||||||
|
<h3> 7. Liens externes</h3>
|
||||||
|
<p>Le site peut contenir des liens vers des sites tiers. Ces liens sont fournis à titre informatif. L’éditeur ne saurait être tenu responsable du contenu, du fonctionnement ou des politiques de confidentialité de ces sites externes.</p>
|
||||||
|
|
||||||
|
<h3> 8. Responsabilité</h3>
|
||||||
|
<p>L’éditeur ne pourra être tenu responsable des dommages directs ou indirects résultant de l’utilisation du site, d’une interruption temporaire, d’un dysfonctionnement technique ou d’une impossibilité d’accès.</p>
|
||||||
|
<p>L’utilisateur demeure seul responsable de l’usage qu’il fait du site et des informations qu’il y consulte.</p>
|
||||||
|
|
||||||
|
<h3> 9. Données personnelles</h3>
|
||||||
|
<p>Le site peut proposer un ou plusieurs formulaires de contact. Les données éventuellement transmises par l’utilisateur sont uniquement destinées au traitement de sa demande, sauf mention contraire clairement indiquée.</p>
|
||||||
|
<p>Conformément à la réglementation applicable, l’utilisateur peut demander l’accès, la rectification ou la suppression de ses données en contactant l’éditeur du site.</p>
|
||||||
|
|
||||||
|
<h3> 10. Sécurité</h3>
|
||||||
|
<p>L’utilisateur s’interdit de perturber le bon fonctionnement du site, d’introduire des programmes malveillants, de tenter un accès non autorisé ou de porter atteinte à l’intégrité technique du service.</p>
|
||||||
|
|
||||||
|
<h3> 11. Modification des CGU</h3>
|
||||||
|
<p>L’éditeur se réserve le droit de modifier à tout moment les présentes CGU. Les modifications prennent effet dès leur publication sur cette page.</p>
|
||||||
|
|
||||||
|
<h3> 12. Droit applicable</h3>
|
||||||
|
<p>Les présentes CGU sont soumises au droit français. En cas de litige, et à défaut de résolution amiable, les juridictions compétentes seront celles déterminées par les règles de droit commun.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once('../elements/footer.php');
|
||||||
|
?>
|
||||||
|
|||||||
+44
-28
@@ -1,41 +1,57 @@
|
|||||||
<?php
|
<?php
|
||||||
// Formulaire de contact avec WEBHOOK Discord
|
// Formulaire de contact avec WEBHOOK Discord
|
||||||
|
|
||||||
|
// Titre de l'onglet
|
||||||
|
$pageTitle = 'Contact';
|
||||||
|
|
||||||
// On ajoute ici le header
|
// On ajoute ici le header
|
||||||
require_once("../elements/header.php");
|
require_once("../elements/header.php");
|
||||||
|
|
||||||
|
// Réglages de l'email et de l'adresse (modifiables et masquables depuis l'admin)
|
||||||
|
require_once('../functions/listSettings.php');
|
||||||
|
|
||||||
|
$contactEmail = recupererSetting('contact_email') ?? 'esteban.moussu@ik.me';
|
||||||
|
$contactEmailEnabled = (recupererSetting('contact_email_enabled') ?? '1') === '1';
|
||||||
|
|
||||||
|
$contactAddressLine1 = recupererSetting('contact_address_line1') ?? '20 Grande rue';
|
||||||
|
$contactAddressLine2 = recupererSetting('contact_address_line2') ?? '52500 Broncourt';
|
||||||
|
$contactAddressEnabled = (recupererSetting('contact_address_enabled') ?? '1') === '1';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<body>
|
<div class="contact-page">
|
||||||
<main class="contact-page">
|
<section class="contact-wrapper">
|
||||||
<section class="contact-wrapper">
|
<div class="contact-card">
|
||||||
<div class="contact-card">
|
<div class="contact-info">
|
||||||
<div class="contact-info">
|
<p class="contact-title">Contact</p>
|
||||||
<p class="contact-title">Contact</p>
|
<p class="contact-name">Esteban MOUSSU</p>
|
||||||
<p class="contact-name">Moussu Esteban</p>
|
<?php if ($contactEmailEnabled && !empty($contactEmail)): ?>
|
||||||
<p class="contact-line">esteban.moussu@ik.me</p>
|
<p class="contact-line"><i class="fa-solid fa-envelope"></i> <?php echo htmlspecialchars($contactEmail); ?></p>
|
||||||
<p class="contact-line">20 Grande rue</p>
|
<?php endif; ?>
|
||||||
<p class="contact-line">52500 Broncourt</p>
|
<?php if ($contactAddressEnabled && (!empty($contactAddressLine1) || !empty($contactAddressLine2))): ?>
|
||||||
|
<p class="contact-line"><i class="fa-solid fa-location-dot"></i> <?php echo htmlspecialchars($contactAddressLine1); ?></p>
|
||||||
|
<p class="contact-line"><?php echo htmlspecialchars($contactAddressLine2); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire d'envoi -->
|
||||||
|
<form class="contact-form" method="POST" action="../scripts/sendMessage.php">
|
||||||
|
<!-- Deux champs de texte, un subject et un message -->
|
||||||
|
<div class="contact-grid">
|
||||||
|
<input type="text" name="subject" id="subject" placeholder="Sujet du message" required>
|
||||||
|
<input type="text" name="name" id="name" placeholder="Nom et prénom" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Formulaire d'envoi -->
|
<input type="email" name="mail" id="mail" placeholder="E-mail de contact" required>
|
||||||
<form class="contact-form" method="POST" action="../scripts/sendMessage.php">
|
<textarea name="message" id="message" placeholder="Votre message" required></textarea>
|
||||||
<!-- Deux champs de texte, un subject et un message -->
|
|
||||||
<div class="contact-grid">
|
|
||||||
<input type="text" name="subject" id="subject" placeholder="Sujet du message" required>
|
|
||||||
<input type="text" name="name" id="name" placeholder="Nom et prénom" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="email" name="mail" id="mail" placeholder="E-mail de contact" required>
|
<!-- Bouton d'envoi -->
|
||||||
<textarea name="message" id="message" placeholder="Votre message" required></textarea>
|
<input type="submit" value="Envoyer le message">
|
||||||
|
</form>
|
||||||
<!-- Bouton d'envoi -->
|
</div>
|
||||||
<input type="submit" value="Envoyer le message">
|
</section>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Ajout du footer
|
// Ajout du footer
|
||||||
require_once("../elements/footer.php");
|
require_once("../elements/footer.php");
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Page de détails d'un projet / d'une expérience / d'une école
|
||||||
|
// Le contenu markdown (stocké en BDD, éditable depuis le panel admin) est rendu
|
||||||
|
// côté navigateur avec marked.js puis nettoyé avec DOMPurify (sécurité XSS)
|
||||||
|
|
||||||
|
// Connexion BDD (avant le header car on doit pouvoir rediriger si l'élément n'existe pas)
|
||||||
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
|
// Types autorisés : liste blanche pour empêcher toute injection de nom de table
|
||||||
|
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6) : on la
|
||||||
|
// joint juste pour le nom de fichier, jamais pour son contenu binaire)
|
||||||
|
$types = [
|
||||||
|
'projet' => ['table' => 'projects', 'pdf' => 'project_pdfs', 'pdf_colonne' => 'project_id', 'retour' => './projects.php', 'label' => 'Projet'],
|
||||||
|
'experience' => ['table' => 'experiences', 'pdf' => 'experience_pdfs', 'pdf_colonne' => 'experience_id', 'retour' => './xp.php', 'label' => 'Expérience'],
|
||||||
|
'ecole' => ['table' => 'schools', 'pdf' => 'school_pdfs', 'pdf_colonne' => 'school_id', 'retour' => './xp.php', 'label' => 'École'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$type = $_GET['type'] ?? '';
|
||||||
|
$id = $_GET['id'] ?? '';
|
||||||
|
|
||||||
|
// Type inconnu ou id invalide : retour à l'accueil
|
||||||
|
if (!isset($types[$type]) || !is_numeric($id)) {
|
||||||
|
header('Location: ./home.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$infos = $types[$type];
|
||||||
|
|
||||||
|
// Récupération de l'élément demandé
|
||||||
|
// (pdf_data n'est volontairement pas sélectionné : le PDF est servi à part par viewPdf.php
|
||||||
|
// dans une <iframe>, inutile de charger son contenu binaire en mémoire ici)
|
||||||
|
$colonnesCommunes = 'e.id, e.title, e.status, e.tags, e.description, e.image_url, e.markdown, e.link_url, p.pdf_filename';
|
||||||
|
$colonnesParType = [
|
||||||
|
'projet' => $colonnesCommunes,
|
||||||
|
'experience' => $colonnesCommunes . ', e.xptype, e.duration',
|
||||||
|
'ecole' => $colonnesCommunes . ', e.duration',
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $dbh->prepare(
|
||||||
|
'SELECT ' . $colonnesParType[$type] . '
|
||||||
|
FROM `' . $infos['table'] . '` e
|
||||||
|
LEFT JOIN `' . $infos['pdf'] . '` p ON p.`' . $infos['pdf_colonne'] . '` = e.id
|
||||||
|
WHERE e.id = :id'
|
||||||
|
);
|
||||||
|
$stmt->bindValue(':id', (int) $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
$element = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$element = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasMarkdown = $element && trim($element['markdown'] ?? '') !== '';
|
||||||
|
$hasPdf = $element && trim($element['pdf_filename'] ?? '') !== '';
|
||||||
|
|
||||||
|
// Élément inexistant, ou sans markdown ni PDF : retour à la liste
|
||||||
|
if (!$element || (!$hasMarkdown && !$hasPdf)) {
|
||||||
|
header('Location: ' . $infos['retour']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Titre de l'onglet
|
||||||
|
$pageTitle = $element['title'];
|
||||||
|
|
||||||
|
// On ajoute le header
|
||||||
|
require_once('../elements/header.php');
|
||||||
|
|
||||||
|
// Préparation des données pour l'affichage
|
||||||
|
$titre = htmlspecialchars($element['title']);
|
||||||
|
$image = htmlspecialchars($element['image_url'] ?? '');
|
||||||
|
$status = htmlspecialchars($element['status'] ?? '');
|
||||||
|
$duration = htmlspecialchars($element['duration'] ?? '');
|
||||||
|
$xptype = htmlspecialchars($element['xptype'] ?? '');
|
||||||
|
$tags = [];
|
||||||
|
if (!empty($element['tags'])) {
|
||||||
|
$tags = array_filter(array_map('trim', explode(',', $element['tags'])));
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="details-page">
|
||||||
|
|
||||||
|
<!-- Bouton retour vers la liste -->
|
||||||
|
<a class="btn btn-ghost details-back" href="<?php echo $infos['retour']; ?>">
|
||||||
|
<i class="fa-solid fa-arrow-left"></i> Retour
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<article class="details-card">
|
||||||
|
|
||||||
|
<?php 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'); ?>
|
||||||
+125
-2
@@ -3,12 +3,135 @@
|
|||||||
// Page Home PortFolio Esteban
|
// Page Home PortFolio Esteban
|
||||||
// Cette page servira de page d'accueil où nous pourrons y retrouver une redirection vers les différents projets, espace de connexion, form de contact, etc...
|
// Cette page servira de page d'accueil où nous pourrons y retrouver une redirection vers les différents projets, espace de connexion, form de contact, etc...
|
||||||
|
|
||||||
|
// Titre de l'onglet
|
||||||
|
$pageTitle = 'Accueil';
|
||||||
|
|
||||||
// Insertion du header
|
// Insertion du header
|
||||||
require_once('./../elements/header.php');
|
require_once('./../elements/header.php');
|
||||||
|
|
||||||
// Ajout d'un texte pour rappeller aux utilisateurs de la démo de vider le cache si du CSS s'affiche mal
|
// La notification est maintenant dynamique : modifiable depuis le panel Admin (table settings)
|
||||||
|
require_once('../functions/listSettings.php');
|
||||||
|
|
||||||
|
// Bandeau "Ils m'ont fait confiance", géré depuis le panel Admin (table logo_banners)
|
||||||
|
require_once('../functions/listLogos.php');
|
||||||
|
|
||||||
|
// Bandeau "Outils que je sais manipuler" : dérivé directement des icônes de la page
|
||||||
|
// Compétences, pas de saisie séparée (voir listeIconesCompetences)
|
||||||
|
require_once('../functions/listSkills.php');
|
||||||
|
|
||||||
|
// Chiffres clés (bandeau de stats sous le hero) : nombre réel de projets/expériences
|
||||||
|
// en BDD, et nombre de technologies reconnaissables (réutilise listeIconesCompetences)
|
||||||
|
require_once('../functions/listStats.php');
|
||||||
|
$statsHome = recupererStatsHome();
|
||||||
|
$statsHome['technologies'] = count(listeIconesCompetences());
|
||||||
|
|
||||||
|
// Affichage de la notification (si elle est activée dans le panel Admin)
|
||||||
|
afficherNotice();
|
||||||
?>
|
?>
|
||||||
<p> Si du CSS / ou des images s'affichent mal, il faut penser à vider le cache, version de développement uniquement.</p>
|
|
||||||
|
<!-- Section hero : présentation rapide -->
|
||||||
|
<section class="hero">
|
||||||
|
<span class="hero-badge"><i class="fa-solid fa-graduation-cap"></i> Étudiant en informatique</span>
|
||||||
|
<h1>Salut, moi c'est <span class="accent">Esteban</span></h1>
|
||||||
|
<p>
|
||||||
|
Passionné par les systèmes, les réseaux et le développement web.
|
||||||
|
Découvre mes projets, mes expériences professionnelles et mes compétences.
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Chiffres clés : n'affiche que les compteurs non nuls (pas de "0 projets" décevant
|
||||||
|
// si le site vient d'être initialisé)
|
||||||
|
$statsAffichables = array_filter([
|
||||||
|
'projets' => ['valeur' => $statsHome['projets'], 'label' => 'Projet' . ($statsHome['projets'] > 1 ? 's' : '') . ' réalisés'],
|
||||||
|
'experiences' => ['valeur' => $statsHome['experiences'], 'label' => 'Expérience' . ($statsHome['experiences'] > 1 ? 's' : '') . ' pro'],
|
||||||
|
'technologies' => ['valeur' => $statsHome['technologies'], 'label' => 'Technologies maîtrisées'],
|
||||||
|
], function ($stat) { return $stat['valeur'] > 0; });
|
||||||
|
?>
|
||||||
|
<?php if (!empty($statsAffichables)): ?>
|
||||||
|
<!-- Bandeau de chiffres clés : compteurs réels, calculés depuis la BDD -->
|
||||||
|
<section class="stats-strip">
|
||||||
|
<?php foreach ($statsAffichables as $stat): ?>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-number"><?php echo (int) $stat['valeur']; ?></span>
|
||||||
|
<span class="stat-label"><?php echo htmlspecialchars($stat['label']); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty(listeIconesCompetences())): ?>
|
||||||
|
<!-- Bandeau défilant des outils, dérivé de la page Compétences -->
|
||||||
|
<section class="logo-banner-section">
|
||||||
|
<div class="logo-banner-header">
|
||||||
|
<h2>Outils que je sais manipuler</h2>
|
||||||
|
</div>
|
||||||
|
<?php afficherOutilsCompetences(); ?>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Section d'exploration rapide -->
|
||||||
|
<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 de mon portfolio.</p>
|
||||||
|
</div>
|
||||||
|
<!-- Grille de cartes de redirection vers les différentes page -->
|
||||||
|
<div class="quick-links-grid">
|
||||||
|
<a href="../pages/projects.php" class="quick-link-card">
|
||||||
|
<div class="quick-link-icon">
|
||||||
|
<i class="fa-solid fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Projets</h3>
|
||||||
|
<p>Voir mes réalisations.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="../pages/xp.php" class="quick-link-card">
|
||||||
|
<div class="quick-link-icon">
|
||||||
|
<i class="fa-solid fa-briefcase"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Expériences</h3>
|
||||||
|
<p>Découvrir mes stages et missions réalisées.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="../pages/skills.php" class="quick-link-card">
|
||||||
|
<div class="quick-link-icon">
|
||||||
|
<i class="fa-solid fa-code"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Compétences</h3>
|
||||||
|
<!-- Page où il y aura toutes mes compétences -->
|
||||||
|
<p>Systèmes, réseaux et développement.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="../pages/contact.php" class="quick-link-card">
|
||||||
|
<div class="quick-link-icon">
|
||||||
|
<i class="fa-solid fa-paper-plane"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Contact</h3>
|
||||||
|
<p>Me contacter rapidement pour échanger.</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php if (!empty(listeLogos('partner'))): ?>
|
||||||
|
<!-- Bandeau défilant des entreprises partenaires, géré depuis le panel Admin -->
|
||||||
|
<section class="logo-banner-section">
|
||||||
|
<div class="logo-banner-header">
|
||||||
|
<h2>Ils m'ont fait confiance</h2>
|
||||||
|
</div>
|
||||||
|
<?php afficherLogos('partner'); ?>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Ajout du footer
|
// Ajout du footer
|
||||||
|
|||||||
+3
-5
@@ -3,6 +3,8 @@
|
|||||||
// Dans l'idée un champ pour un nom d'utilisateur, et un pour le mot de passe puis un bouton de connexion
|
// Dans l'idée un champ pour un nom d'utilisateur, et un pour le mot de passe puis un bouton de connexion
|
||||||
// Egalement permettre à l'utilisateur de faire retour en arrière s'il s'est trompé (croix ou header)
|
// Egalement permettre à l'utilisateur de faire retour en arrière s'il s'est trompé (croix ou header)
|
||||||
|
|
||||||
|
// Titre de l'onglet
|
||||||
|
$pageTitle = 'Connexion';
|
||||||
|
|
||||||
// On ajoute ici le Header
|
// On ajoute ici le Header
|
||||||
require_once('./../elements/header.php');
|
require_once('./../elements/header.php');
|
||||||
@@ -15,8 +17,6 @@ if (isset($_SESSION['user_id'])){
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- CSS Login (fait par Intelligence Arti) -->
|
<!-- CSS Login (fait par Intelligence Arti) -->
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
@@ -37,9 +37,7 @@ if (isset($_SESSION['user_id'])){
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Ajout du footer
|
// Ajout du footer
|
||||||
require_once('../elements/footer.php'); ?>
|
require_once('../elements/footer.php'); ?>
|
||||||
|
|||||||
+10
-4
@@ -1,19 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Page affichant les différents projets personnels / scolaires / professionnels
|
// Page affichant les différents projets personnels / scolaires / professionnels
|
||||||
// On pourrait également rajouter des tags comme "En cours" ou même "En reflexion"...
|
// On pourrait également rajouter des tags comme "En cours" ou même "En reflexion"...
|
||||||
|
|
||||||
|
// Titre de l'onglet
|
||||||
|
$pageTitle = 'Projets';
|
||||||
|
|
||||||
// On ajoute le header
|
// On ajoute le header
|
||||||
require_once('../elements/header.php');
|
require_once('../elements/header.php');
|
||||||
|
|
||||||
// Ajout d'un message indiquant si l'utilisateur est admin qu'il peut modifier ces projets
|
// Ajout d'un message indiquant si l'utilisateur est admin qu'il peut modifier ces projets
|
||||||
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
||||||
echo "<p style='color: white; text-align: center;'>Vous êtes administrateur, vous pouvez modifier les projets <a href='../pages/admin.php'>ici</a>.</p>";
|
echo "<p class='admin-notice'>Vous êtes administrateur, vous pouvez modifier les projets <a href='../pages/admin.php'>ici</a>.</p>";
|
||||||
}
|
}
|
||||||
// Affichages des projets
|
// Affichages des projets
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<p class="projects-title">Liste des projets</p>
|
<div class="page-header">
|
||||||
|
<h1>Projets</h1>
|
||||||
|
<p>Mes réalisations personnelles, scolaires et professionnelles.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
@@ -24,4 +30,4 @@ require_once('../functions/listProject.php');
|
|||||||
afficherProjets();
|
afficherProjets();
|
||||||
|
|
||||||
// Ajout du footer
|
// Ajout du footer
|
||||||
require_once('../elements/footer.php'); ?>
|
require_once('../elements/footer.php'); ?>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Compétences : le contenu est maintenant dynamique, géré depuis le panel Admin (table skills)
|
||||||
|
|
||||||
|
// Titre de l'onglet
|
||||||
|
$pageTitle = 'Compétences';
|
||||||
|
|
||||||
|
// On ajoute ici le header
|
||||||
|
require_once('./../elements/header.php');
|
||||||
|
|
||||||
|
// Ajout d'un message indiquant si l'utilisateur est admin qu'il peut modifier ces compétences
|
||||||
|
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
||||||
|
echo "<p class='admin-notice'>Vous êtes administrateur, vous pouvez modifier les compétences <a href='../pages/admin.php'>ici</a>.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Contenu de la page skills -->
|
||||||
|
<section class="skills-section">
|
||||||
|
|
||||||
|
<div class="skills-header">
|
||||||
|
<h1>Compétences</h1>
|
||||||
|
<p>Un aperçu des technologies et des domaines que je maîtrise.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once('../functions/listSkills.php');
|
||||||
|
|
||||||
|
// Appel de ma fonction dans Functions/listSkills.php
|
||||||
|
afficherSkills();
|
||||||
|
?>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Ajout du footer
|
||||||
|
require_once('../elements/footer.php'); ?>
|
||||||
+67
-4
@@ -1,19 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Page affichant les différentes expériences pro (stages etc...)
|
// Page affichant les différentes expériences pro (stages etc...)
|
||||||
|
|
||||||
|
// Titre de l'onglet
|
||||||
|
$pageTitle = 'Expériences';
|
||||||
|
|
||||||
// On ajoute le header
|
// On ajoute le header
|
||||||
require_once('../elements/header.php');
|
require_once('../elements/header.php');
|
||||||
|
|
||||||
// Ajout d'un message indiquant si l'utilisateur est admin qu'il peut modifier ces expériences
|
// Ajout d'un message indiquant si l'utilisateur est admin qu'il peut modifier ces expériences
|
||||||
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
||||||
echo "<p style='color: white; text-align: center;'>Vous êtes administrateur, vous pouvez modifier les experiences <a href='../pages/admin.php'>ici</a>.</p>";
|
echo "<p class='admin-notice'>Vous êtes administrateur, vous pouvez modifier les experiences <a href='../pages/admin.php'>ici</a>.</p>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Affichages des xp pro
|
// Affichages des xp pro
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<p class="projects-title">Liste des expériences professionnelles</p>
|
<div class="page-header" id="experiences">
|
||||||
|
<h1>Expériences professionnelles</h1>
|
||||||
|
<p>Mes stages et missions réalisées en entreprise.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
require_once('../functions/listXP.php');
|
require_once('../functions/listXP.php');
|
||||||
@@ -21,6 +27,63 @@ require_once('../functions/listXP.php');
|
|||||||
|
|
||||||
// Appel de ma fonction dans Functions/listXP.php
|
// Appel de ma fonction dans Functions/listXP.php
|
||||||
afficherXP();
|
afficherXP();
|
||||||
|
require_once('../functions/listSchool.php');
|
||||||
|
|
||||||
|
// Section écoles / formations, en dessous des expériences professionnelles
|
||||||
|
// On n'affiche le titre que s'il y a au moins une école (table remplie)
|
||||||
|
if (!empty(listeEcoles())) {
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header" id="ecoles">
|
||||||
|
<h2>Mes écoles</h2>
|
||||||
|
<p>Mon parcours scolaire et mes formations.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?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
|
// Ajout du footer
|
||||||
require_once('../elements/footer.php'); ?>
|
require_once('../elements/footer.php'); ?>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
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') {
|
||||||
|
header('Location: ../pages/home.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
|
// Récupération des données dans la requete POST
|
||||||
|
// La catégorie est restreinte à 'tool' ou 'partner' (sinon on retombe sur 'tool')
|
||||||
|
$category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool';
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$icon = trim($_POST['icon'] ?? '');
|
||||||
|
$logo_url = trim($_POST['logourl'] ?? '');
|
||||||
|
$link_url = trim($_POST['linkurl'] ?? '');
|
||||||
|
$position = (int) ($_POST['position'] ?? 0);
|
||||||
|
|
||||||
|
// On vérifie que les champs obligatoires ne sont pas vides : il faut un nom, et
|
||||||
|
// au moins l'un des deux visuels (icône Devicon ou image)
|
||||||
|
if (!empty($name) && (!empty($icon) || !empty($logo_url))) {
|
||||||
|
try {
|
||||||
|
// On prépare la requête SQL pour insérer le nouveau logo
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO logo_banners (category, name, icon, logo_url, link_url, position) VALUES (:category, :name, :icon, :logo_url, :link_url, :position)');
|
||||||
|
|
||||||
|
// On execute la requete préparée
|
||||||
|
$stmt->execute([
|
||||||
|
':category' => $category,
|
||||||
|
':name' => $name,
|
||||||
|
':icon' => $icon !== '' ? $icon : null,
|
||||||
|
':logo_url' => $logo_url !== '' ? $logo_url : null,
|
||||||
|
':link_url' => $link_url,
|
||||||
|
':position' => $position
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Ajout d'un logo (" . $category . ") : " . $name);
|
||||||
|
|
||||||
|
// Redirection vers home.php dès que la requete est executée
|
||||||
|
header('Location: ../pages/home.php');
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// En cas d'erreur :
|
||||||
|
echo "Erreur pendant l'ajout du logo: " . $e->getMessage();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "Veuillez remplir le nom et au moins une icône ou une image.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si on y accède de façon peu légitime sans requête POST on refuse
|
||||||
|
header('Location: ../pages/home.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
+52
-10
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
require_once '../functions/databaseConnection.php';
|
require_once '../functions/databaseConnection.php';
|
||||||
|
require_once('../functions/sendLogs.php');
|
||||||
|
require_once('../functions/pdfUpload.php');
|
||||||
|
|
||||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||||
@@ -8,29 +10,69 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
|
// tout de suite pour éviter un échec silencieux
|
||||||
|
if (postDepasseLimite()) {
|
||||||
|
die(messageDepassementPost());
|
||||||
|
}
|
||||||
|
|
||||||
// La requête doit être un POST sinon on l'a refuse
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
// Récupération des données dans la requete POST
|
// Récupération des données dans la requete POST
|
||||||
$title = $_POST['title'] ?? '';
|
$title = $_POST['title'] ?? '';
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
$tags = $_POST['tags'] ?? '';
|
$tags = $_POST['tags'] ?? '';
|
||||||
$description = $_POST['description'] ?? '';
|
$description = $_POST['description'] ?? '';
|
||||||
$image_url = $_POST['imageurl'] ?? '';
|
$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
|
// On vérifie que les champs obligatoires ne sont pas vide
|
||||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||||
try {
|
try {
|
||||||
// On prépare la requête SQL pour insérer le nouveau projet
|
// On prépare la requête SQL pour insérer le nouveau projet
|
||||||
$stmt = $dbh->prepare('INSERT INTO projects (title, tags, description, image_url) VALUES (:title, :tags, :description, :image_url)');
|
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans project_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), pour que les listes de projets
|
||||||
|
// n'aient jamais à charger son contenu binaire en mémoire
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO projects (title, status, tags, description, image_url, markdown, link_url) VALUES (:title, :status, :tags, :description, :image_url, :markdown, :link_url)');
|
||||||
|
|
||||||
// On execute la requete préparée
|
// On execute la requete préparée
|
||||||
$stmt->execute([
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
':title' => $title,
|
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||||
':tags' => $tags,
|
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||||
':description' => $description,
|
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||||
':image_url' => $image_url
|
$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->execute();
|
||||||
|
|
||||||
|
// Si un PDF a été envoyé, on l'enregistre à part dans project_pdfs
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
$nouvelId = (int) $dbh->lastInsertId();
|
||||||
|
$stmtPdf = $dbh->prepare('INSERT INTO project_pdfs (project_id, pdf_filename, pdf_data) VALUES (:project_id, :pdf_filename, :pdf_data)');
|
||||||
|
$stmtPdf->bindValue(':project_id', $nouvelId, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Ajout du projet : " . $title);
|
||||||
|
|
||||||
// Redirection vers projects.php ds que la requete est executée
|
// Redirection vers projects.php ds que la requete est executée
|
||||||
header('Location: ../pages/projects.php');
|
header('Location: ../pages/projects.php');
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
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') {
|
||||||
|
header('Location: ../pages/home.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
|
// 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
|
||||||
|
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans school_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), pour que les listes d'écoles
|
||||||
|
// n'aient jamais à charger son contenu binaire en mémoire
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO schools (title, status, tags, description, duration, image_url, markdown, link_url) VALUES (:title, :status, :tags, :description, :duration, :image_url, :markdown, :link_url)');
|
||||||
|
|
||||||
|
// On execute la requete préparée
|
||||||
|
$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->execute();
|
||||||
|
|
||||||
|
// Si un PDF a été envoyé, on l'enregistre à part dans school_pdfs
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
$nouvelId = (int) $dbh->lastInsertId();
|
||||||
|
$stmtPdf = $dbh->prepare('INSERT INTO school_pdfs (school_id, pdf_filename, pdf_data) VALUES (:school_id, :pdf_filename, :pdf_data)');
|
||||||
|
$stmtPdf->bindValue(':school_id', $nouvelId, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Ajout de l'école : " . $title);
|
||||||
|
|
||||||
|
// Redirection vers xp.php dès que la requete est executée
|
||||||
|
header('Location: ../pages/xp.php');
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// En cas d'erreur :
|
||||||
|
echo "Erreur pendant l'ajout de l'école: " . $e->getMessage();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "Veuillez remplir tous les champs obligatoires.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si on y accède de façon peu légitime sans requête POST on refuse
|
||||||
|
header('Location: ../pages/xp.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
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') {
|
||||||
|
header('Location: ../pages/home.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
|
// Récupération des données dans la requete POST
|
||||||
|
$title = trim($_POST['title'] ?? '');
|
||||||
|
$icon = trim($_POST['icon'] ?? '');
|
||||||
|
$items = trim($_POST['items'] ?? '');
|
||||||
|
$position = (int) ($_POST['position'] ?? 0);
|
||||||
|
|
||||||
|
// On vérifie que les champs obligatoires ne sont pas vide
|
||||||
|
if (!empty($title) && !empty($items)) {
|
||||||
|
try {
|
||||||
|
// On prépare la requête SQL pour insérer la nouvelle catégorie de compétences
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO skills (title, icon, items, position) VALUES (:title, :icon, :items, :position)');
|
||||||
|
|
||||||
|
// On execute la requete préparée
|
||||||
|
$stmt->execute([
|
||||||
|
':title' => $title,
|
||||||
|
':icon' => $icon,
|
||||||
|
':items' => $items,
|
||||||
|
':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;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// En cas d'erreur :
|
||||||
|
echo "Erreur pendant l'ajout de la compétence: " . $e->getMessage();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "Veuillez remplir tous les champs obligatoires.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si on y accède de façon peu légitime sans requête POST on refuse
|
||||||
|
header('Location: ../pages/skills.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
+9
-1
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// Ajout de connexion à la BDD
|
// Ajout de connexion à la BDD
|
||||||
require_once("../functions/databaseConnection.php");
|
require_once("../functions/databaseConnection.php");
|
||||||
|
// Ajout des logs Discord
|
||||||
|
require_once("../functions/sendLogs.php");
|
||||||
|
|
||||||
// Vérification que l'utilisateur est bien admin
|
// Vérification que l'utilisateur est bien admin
|
||||||
if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||||
@@ -10,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Si le formulaire a été soumis
|
// Si le formulaire a été soumis
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||||
// Récupération des données du formulaire
|
// Récupération des données du formulaire
|
||||||
@@ -58,6 +63,9 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
|||||||
":created_at" => $dateCreation
|
":created_at" => $dateCreation
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Création du compte : " . $user . " (rôle : " . $role . ")");
|
||||||
|
|
||||||
// Redirection vers la page admin après succès
|
// Redirection vers la page admin après succès
|
||||||
header("Location: ../pages/admin.php");
|
header("Location: ../pages/admin.php");
|
||||||
exit();
|
exit();
|
||||||
|
|||||||
+53
-12
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
require_once '../functions/databaseConnection.php';
|
require_once '../functions/databaseConnection.php';
|
||||||
|
require_once('../functions/sendLogs.php');
|
||||||
|
require_once('../functions/pdfUpload.php');
|
||||||
|
|
||||||
// Pour faire cette fonction l'utilisateur doit être administrateur
|
// Pour faire cette fonction l'utilisateur doit être administrateur
|
||||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||||
@@ -8,33 +10,72 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
|
// tout de suite pour éviter un échec silencieux
|
||||||
|
if (postDepasseLimite()) {
|
||||||
|
die(messageDepassementPost());
|
||||||
|
}
|
||||||
|
|
||||||
// La requête doit être un POST sinon on l'a refuse
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
// Récupération des données dans la requete POST
|
// Récupération des données dans la requete POST
|
||||||
$title = $_POST['title'] ?? '';
|
$title = $_POST['title'] ?? '';
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
$xptype = $_POST['xptype'] ?? '';
|
$xptype = $_POST['xptype'] ?? '';
|
||||||
$tags = $_POST['tags'] ?? '';
|
$tags = $_POST['tags'] ?? '';
|
||||||
$description = $_POST['description'] ?? '';
|
$description = $_POST['description'] ?? '';
|
||||||
$duration = $_POST['duration'] ?? '';
|
$duration = $_POST['duration'] ?? '';
|
||||||
$image_url = $_POST['imageurl'] ?? '';
|
$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
|
// On vérifie que les champs obligatoires ne sont pas vide
|
||||||
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
if (!empty($title) && !empty($description) && !empty($image_url)) {
|
||||||
try {
|
try {
|
||||||
// On prépare la requête SQL pour insérer la nouvelle experience
|
// On prépare la requête SQL pour insérer la nouvelle experience
|
||||||
$stmt = $dbh->prepare('INSERT INTO experiences (title, xptype, tags, description, duration, image_url) VALUES (:title, :xptype, :tags, :description, :duration, :image_url)');
|
// Le PDF (pdf_data) n'est plus stocké ici : il vit dans experience_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), pour que les listes d'expériences
|
||||||
|
// n'aient jamais à charger son contenu binaire en mémoire
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO experiences (title, status, xptype, tags, description, duration, image_url, markdown, link_url) VALUES (:title, :status, :xptype, :tags, :description, :duration, :image_url, :markdown, :link_url)');
|
||||||
|
|
||||||
// On execute la requete préparée
|
// On execute la requete préparée
|
||||||
$stmt->execute([
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
':title' => $title,
|
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||||
':xptype' => $xptype,
|
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
||||||
':tags' => $tags,
|
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||||
':description' => $description,
|
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||||
':duration' => $duration,
|
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||||
':image_url' => $image_url
|
$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->execute();
|
||||||
|
|
||||||
|
// Si un PDF a été envoyé, on l'enregistre à part dans experience_pdfs
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
$nouvelId = (int) $dbh->lastInsertId();
|
||||||
|
$stmtPdf = $dbh->prepare('INSERT INTO experience_pdfs (experience_id, pdf_filename, pdf_data) VALUES (:experience_id, :pdf_filename, :pdf_data)');
|
||||||
|
$stmtPdf->bindValue(':experience_id', $nouvelId, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Ajout de l'expérience : " . $title);
|
||||||
|
|
||||||
// Redirection vers xp.php ds que la requete est executée
|
// Redirection vers xp.php ds que la requete est executée
|
||||||
header('Location: ../pages/xp.php');
|
header('Location: ../pages/xp.php');
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
+16
-2
@@ -3,11 +3,14 @@
|
|||||||
// Verification de l'utilisateur voir si ces ids sont bon ou pas
|
// Verification de l'utilisateur voir si ces ids sont bon ou pas
|
||||||
|
|
||||||
// On démarre une session au cas où le script soit exécuté directement par URL
|
// On démarre une session au cas où le script soit exécuté directement par URL
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
|
||||||
// Ajout de connexion a la bdd
|
// Ajout de connexion a la bdd
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
|
|
||||||
|
// Ajout des logs Discord
|
||||||
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
// La requête doit être un POST sinon on l'a refuse
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
header('Location: ./../pages/login.php');
|
header('Location: ./../pages/login.php');
|
||||||
@@ -29,10 +32,17 @@ try {
|
|||||||
|
|
||||||
// On regarde si le mdp correspond à celui en bdd
|
// On regarde si le mdp correspond à celui en bdd
|
||||||
if (!$user || !password_verify($password, $user['password'])) {
|
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');
|
header('Location: ./../pages/login.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Régénère l'ID de session à la connexion : empêche la fixation de session
|
||||||
|
// (un attaquant qui aurait imposé un ID de session à la victime avant sa
|
||||||
|
// connexion ne peut plus récupérer une session authentifiée avec cet ID)
|
||||||
|
session_regenerate_id(true);
|
||||||
|
|
||||||
// Si la connexion est ok, on entregistre l'utilisateur dans $_SESSION
|
// Si la connexion est ok, on entregistre l'utilisateur dans $_SESSION
|
||||||
$_SESSION['user_id'] = $user['id'];
|
$_SESSION['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
@@ -46,7 +56,11 @@ try {
|
|||||||
error_log('Le serveur WEB ne peut pas modifier la date de dernière connexion : ' . $e->getMessage());
|
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é
|
//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;
|
exit;
|
||||||
?>
|
?>
|
||||||
+23
-5
@@ -2,13 +2,31 @@
|
|||||||
|
|
||||||
// Lorsque l'utilisateur est renvoyé à cette page/fonction, il est déconnecté
|
// Lorsque l'utilisateur est renvoyé à cette page/fonction, il est déconnecté
|
||||||
|
|
||||||
|
// Démarrage de session (cookie durci)
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
|
||||||
// Démarrage de session
|
// Ajout des logs Discord
|
||||||
session_start();
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
// Destruction des variables de session
|
// 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;
|
||||||
|
|
||||||
|
// Destruction des variables de session
|
||||||
$_SESSION = array();
|
$_SESSION = array();
|
||||||
session_destroy();
|
session_destroy();
|
||||||
|
|
||||||
// Redirection de l'utilisateur vers home.php
|
// Si le captcha avait déjà été validé, on recrée une session propre
|
||||||
header('Location: ../index.php');
|
// pour conserver uniquement cette information
|
||||||
|
if ($captchaAlreadyValid) {
|
||||||
|
configureSecureSessionCookies();
|
||||||
|
session_start();
|
||||||
|
$_SESSION['captcha_valid'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirection de l'utilisateur vers l'accueil
|
||||||
|
// (?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,87 @@
|
|||||||
|
<?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
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// Liste blanche des tables et colonnes exportées (empêche toute injection via noms de colonnes)
|
||||||
|
// Depuis la migration Juillet 2026 (6), les PDF vivent dans des tables associées
|
||||||
|
// (project_pdfs, experience_pdfs, school_pdfs) plutôt que dans projects/experiences/schools :
|
||||||
|
// elles sont placées juste après leur table parente (l'ordre compte pour l'import, à cause
|
||||||
|
// des clés étrangères ON DELETE CASCADE)
|
||||||
|
$tables = [
|
||||||
|
'settings' => ['name', 'value'],
|
||||||
|
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||||
|
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
|
'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
|
'experience_pdfs' => ['experience_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
|
'school_pdfs' => ['school_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Colonnes binaires : le JSON ne peut pas contenir de binaire brut, on les encode en base64
|
||||||
|
// (+33% de taille mais nécessaire pour que le PDF survive à un export/import complet)
|
||||||
|
$colonnesBinaires = [
|
||||||
|
'project_pdfs' => ['pdf_data'],
|
||||||
|
'experience_pdfs' => ['pdf_data'],
|
||||||
|
'school_pdfs' => ['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,126 @@
|
|||||||
|
<?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
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// La requête doit être un POST avec un fichier sinon on refuse
|
||||||
|
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.
|
||||||
|
// L'ORDRE COMPTE : chaque table de PDF associée (project_pdfs...) doit être importée
|
||||||
|
// juste après sa table parente (projects...), car sa clé étrangère exige que la ligne
|
||||||
|
// parente existe déjà (voir migration Juillet 2026 (6))
|
||||||
|
$tables = [
|
||||||
|
'settings' => ['name', 'value'],
|
||||||
|
'skills' => ['id', 'title', 'icon', 'items', 'position', 'created_at'],
|
||||||
|
'projects' => ['id', 'title', 'status', 'tags', 'description', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
|
'project_pdfs' => ['project_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'experiences' => ['id', 'title', 'status', 'xptype', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
|
'experience_pdfs' => ['experience_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'schools' => ['id', 'title', 'status', 'tags', 'description', 'duration', 'image_url', 'markdown', 'link_url', 'created_at'],
|
||||||
|
'school_pdfs' => ['school_id', 'pdf_filename', 'pdf_data'],
|
||||||
|
'users' => ['id', 'username', 'password', 'role', 'created_at', 'last_login'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Colonnes binaires : encodées en base64 par exportData.php, à redécoder ici
|
||||||
|
$colonnesBinaires = [
|
||||||
|
'project_pdfs' => ['pdf_data'],
|
||||||
|
'experience_pdfs' => ['pdf_data'],
|
||||||
|
'school_pdfs' => ['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;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
// On démarre la session
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
|
header('Location: ../pages/admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On collecte l'id et on le met en variable locale
|
||||||
|
$id = (int) $_GET['id'];
|
||||||
|
|
||||||
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
|
$stmt = $dbh->prepare('DELETE FROM logo_banners WHERE id = :id');
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Erreur lors de la suppression du logo :" . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Suppression du logo id " . $id);
|
||||||
|
|
||||||
|
// On renvoie l'utilisateur sur admin.php, directement sur la section Logos
|
||||||
|
header('Location: ../pages/admin.php#admin-logos');
|
||||||
|
exit;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
|
// On ajoute les logs Discord
|
||||||
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||||
@@ -10,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
@@ -26,6 +31,9 @@ try { // On essaie d'executer une requête qui est préparée
|
|||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur lors de la suppression du projet :" . $e->getMessage());
|
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
|
// On renvoie l'utilisateur sur admin.php
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
exit;
|
exit;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
// On démarre la session
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
|
header('Location: ../pages/admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On collecte l'id et on le met en variable locale
|
||||||
|
$id = (int) $_GET['id'];
|
||||||
|
|
||||||
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
|
$stmt = $dbh->prepare('DELETE FROM schools WHERE id = :id');
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
} 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;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
// On démarre la session
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
|
header('Location: ../pages/admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On collecte l'id et on le met en variable locale
|
||||||
|
$id = (int) $_GET['id'];
|
||||||
|
|
||||||
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
|
$stmt = $dbh->prepare('DELETE FROM skills WHERE id = :id');
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
} 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;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
// Démarrage de la session pour vérifier les droits de l'utilisateur
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// Ajout de connexion à la BDD
|
// Ajout de connexion à la BDD
|
||||||
require_once("../functions/databaseConnection.php");
|
require_once("../functions/databaseConnection.php");
|
||||||
|
// Ajout des logs Discord
|
||||||
|
require_once("../functions/sendLogs.php");
|
||||||
|
|
||||||
// Vérification que l'utilisateur est bien admin
|
// Vérification que l'utilisateur est bien admin
|
||||||
if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
||||||
@@ -10,6 +12,9 @@ if (!isset($_SESSION["role"]) || $_SESSION["role"] != "admin") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
// Récupération de l'ID passé en paramètre GET
|
// Récupération de l'ID passé en paramètre GET
|
||||||
$id = $_GET["id"];
|
$id = $_GET["id"];
|
||||||
|
|
||||||
@@ -26,6 +31,9 @@ if ($id == "") {
|
|||||||
":id" => $id
|
":id" => $id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Suppression du compte id " . $id);
|
||||||
|
|
||||||
// Redirection vers la page admin après suppression
|
// Redirection vers la page admin après suppression
|
||||||
header("Location: ../pages/admin.php");
|
header("Location: ../pages/admin.php");
|
||||||
exit();
|
exit();
|
||||||
|
|||||||
+10
-1
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
|
// On ajoute les logs Discord
|
||||||
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||||
@@ -10,6 +12,9 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
//L'id doit être collecté sinon l'utilisateur ne peux pas executer le reste du script
|
||||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
@@ -26,6 +31,10 @@ try { // On essaie d'executer une requête qui est préparée
|
|||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur lors de la suppression de l experience :" . $e->getMessage());
|
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
|
// On renvoie l'utilisateur sur admin.php
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
exit;
|
exit;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/../functions/listSettings.php');
|
||||||
|
|
||||||
|
// Le captcha peut être désactivé depuis le panel Admin (section Sécurité)
|
||||||
|
$captchaEnabled = recupererSetting('captcha_enabled') ?? '1';
|
||||||
|
|
||||||
|
if ($captchaEnabled === '1' && (!isset($_SESSION['captcha_valid']) || $_SESSION['captcha_valid'] !== true)) {
|
||||||
|
header('Location: ../captcha.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
+14
-3
@@ -1,6 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// 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
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
require_once('../functions/sendLogs.php');
|
||||||
|
|
||||||
// Envoi de formulaire de contact via WebHook discord
|
// Envoi de formulaire de contact via WebHook discord
|
||||||
$webhook = "https://discord.com/api/webhooks/1513445938033721399/2NxoIw5TYCH7Rei0tyw1lEswmuYizTkgNPIaPe0gmSqvlXNub7qz_uora3duTUxPg8vm";
|
$webhook = ID_DISCORD2; // URL du webhook Discord
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
|
||||||
// Exemple du message :
|
// Exemple du message :
|
||||||
@@ -49,12 +59,13 @@ if ($subject == "" || $name == "" || $mail == "" || $message == "") {
|
|||||||
$resultat = curl_exec($ch);
|
$resultat = curl_exec($ch);
|
||||||
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$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é
|
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");
|
header("Location: ../pages/contact.php?success=1");
|
||||||
exit();
|
exit();
|
||||||
} else { // Sinon c'est que ça n'a pas été envoyé (discord down / webhook expiré / etcc...)
|
} 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.";
|
echo "Le message n'a pas pu être envoyé sur Discord.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
// On démarre la session
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||||
|
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||||
|
empty($_POST['name']) ||
|
||||||
|
(empty($_POST['icon']) && empty($_POST['logourl']))
|
||||||
|
) {
|
||||||
|
header('Location: ../pages/admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On récupère les données pour les mettres en variables locales
|
||||||
|
$id = (int) $_POST['id'];
|
||||||
|
$category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool';
|
||||||
|
$name = trim($_POST['name']);
|
||||||
|
$icon = trim($_POST['icon'] ?? '');
|
||||||
|
$logourl = trim($_POST['logourl'] ?? '');
|
||||||
|
$linkurl = trim($_POST['linkurl'] ?? '');
|
||||||
|
$position = (int) ($_POST['position'] ?? 0);
|
||||||
|
|
||||||
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
UPDATE logo_banners
|
||||||
|
SET category = :category, name = :name, icon = :icon, logo_url = :logo_url, link_url = :link_url, position = :position
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
$stmt->bindValue(':category', $category, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':name', $name, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':icon', $icon !== '' ? $icon : null, $icon !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||||
|
$stmt->bindValue(':logo_url', $logourl !== '' ? $logourl : null, $logourl !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||||
|
$stmt->bindValue(':link_url', $linkurl, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':position', $position, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Erreur lors de la modification du logo :" . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Modification du logo : " . $name . " (id " . $id . ")");
|
||||||
|
|
||||||
|
// On renvoie l'utilisateur sur admin.php, directement sur la section Logos
|
||||||
|
header('Location: ../pages/admin.php#admin-logos');
|
||||||
|
exit;
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
|
// On ajoute les logs Discord
|
||||||
|
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
|
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||||
@@ -10,6 +13,15 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
|
// 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
|
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||||
empty($_POST['title']) ||
|
empty($_POST['title']) ||
|
||||||
@@ -23,25 +35,77 @@ if ( // Si les champs sont vides on renvoie vers admin.php
|
|||||||
// On récupère les données pour les mettres en variables locales
|
// On récupère les données pour les mettres en variables locales
|
||||||
$id = (int) $_POST['id'];
|
$id = (int) $_POST['id'];
|
||||||
$title = trim($_POST['title']);
|
$title = trim($_POST['title']);
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
$tags = trim($_POST['tags'] ?? '');
|
$tags = trim($_POST['tags'] ?? '');
|
||||||
$description = trim($_POST['description']);
|
$description = trim($_POST['description']);
|
||||||
$imageurl = trim($_POST['imageurl']);
|
$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
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
|
// Le PDF (pdf_data) n'est plus stocké dans projects : il vit dans project_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||||
$stmt = $dbh->prepare('
|
$stmt = $dbh->prepare('
|
||||||
UPDATE projects
|
UPDATE projects
|
||||||
SET title = :title, tags = :tags, description = :description, image_url = :image_url
|
SET title = :title, status = :status, tags = :tags, description = :description, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
');
|
');
|
||||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
|
||||||
|
if ($updatePdf) {
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = project_id)
|
||||||
|
$stmtPdf = $dbh->prepare('
|
||||||
|
INSERT INTO project_pdfs (project_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||||
|
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||||
|
');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
} else {
|
||||||
|
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||||
|
$stmtPdf = $dbh->prepare('DELETE FROM project_pdfs WHERE project_id = :id');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur lors de la modification du projet :" . $e->getMessage());
|
die("Erreur lors de la modification du projet :" . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Modification du projet : " . $title . " (id " . $id . ")");
|
||||||
|
|
||||||
// On renvoie l'utilisateur sur admin.php
|
// On renvoie l'utilisateur sur admin.php
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
exit;
|
exit;
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
// On démarre la session
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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') {
|
||||||
|
header('Location: ../pages/home.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
|
// 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']) ||
|
||||||
|
empty($_POST['description']) ||
|
||||||
|
empty($_POST['duration']) ||
|
||||||
|
empty($_POST['imageurl'])
|
||||||
|
) {
|
||||||
|
header('Location: ../pages/admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// Le PDF (pdf_data) n'est plus stocké dans schools : il vit dans school_pdfs (table
|
||||||
|
// associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
UPDATE schools
|
||||||
|
SET title = :title, status = :status, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
$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);
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
|
||||||
|
if ($updatePdf) {
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = school_id)
|
||||||
|
$stmtPdf = $dbh->prepare('
|
||||||
|
INSERT INTO school_pdfs (school_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||||
|
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||||
|
');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
} else {
|
||||||
|
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||||
|
$stmtPdf = $dbh->prepare('DELETE FROM school_pdfs WHERE school_id = :id');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
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;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
// On démarre la session
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// La requête doit être un POST sinon on l'a refuse
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
header('Location: ../pages/admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On récupère les données pour les mettres en variables locales
|
||||||
|
// La checkbox n'est envoyée que si elle est cochée
|
||||||
|
// settings_group : évite qu'un formulaire écrase les réglages d'un autre (ex : la case
|
||||||
|
// "notice_enabled" non cochée ne doit pas désactiver le captcha, et inversement)
|
||||||
|
$groupeReglages = $_POST['settings_group'] ?? 'notice';
|
||||||
|
|
||||||
|
if ($groupeReglages === 'captcha') {
|
||||||
|
$reglages = [
|
||||||
|
'captcha_enabled' => isset($_POST['captcha_enabled']) ? '1' : '0'
|
||||||
|
];
|
||||||
|
} elseif ($groupeReglages === 'contact') {
|
||||||
|
$reglages = [
|
||||||
|
'contact_email' => trim($_POST['contact_email'] ?? ''),
|
||||||
|
'contact_email_enabled' => isset($_POST['contact_email_enabled']) ? '1' : '0',
|
||||||
|
'contact_address_line1' => trim($_POST['contact_address_line1'] ?? ''),
|
||||||
|
'contact_address_line2' => trim($_POST['contact_address_line2'] ?? ''),
|
||||||
|
'contact_address_enabled' => isset($_POST['contact_address_enabled']) ? '1' : '0'
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$reglages = [
|
||||||
|
'site_notice_title' => trim($_POST['notice_title'] ?? ''),
|
||||||
|
'site_notice_text' => trim($_POST['notice_text'] ?? ''),
|
||||||
|
'site_notice_enabled' => isset($_POST['notice_enabled']) ? '1' : '0'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try { // On essaie d'executer une requête qui est préparée pour chaque réglage
|
||||||
|
// ON DUPLICATE KEY UPDATE : crée le réglage s'il n'existe pas encore, le met à jour sinon
|
||||||
|
$stmt = $dbh->prepare('INSERT INTO settings (name, value) VALUES (:name, :value) ON DUPLICATE KEY UPDATE value = VALUES(value)');
|
||||||
|
|
||||||
|
foreach ($reglages as $name => $value) {
|
||||||
|
$stmt->execute([
|
||||||
|
':name' => $name,
|
||||||
|
':value' => $value
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Erreur lors de la modification de la notification :" . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
$libellesGroupes = [
|
||||||
|
'captcha' => 'Modification du réglage du captcha',
|
||||||
|
'contact' => 'Modification des coordonnées de contact',
|
||||||
|
'notice' => "Modification de la notification de la page d'accueil"
|
||||||
|
];
|
||||||
|
sendLog($libellesGroupes[$groupeReglages] ?? "Modification des réglages du site");
|
||||||
|
|
||||||
|
// On renvoie l'utilisateur sur admin.php
|
||||||
|
header('Location: ../pages/admin.php');
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
// On démarre la session
|
||||||
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
if ( // Si les champs sont vides on renvoie vers admin.php
|
||||||
|
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||||
|
empty($_POST['title']) ||
|
||||||
|
empty($_POST['items'])
|
||||||
|
) {
|
||||||
|
header('Location: ../pages/admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On récupère les données pour les mettres en variables locales
|
||||||
|
$id = (int) $_POST['id'];
|
||||||
|
$title = trim($_POST['title']);
|
||||||
|
$icon = trim($_POST['icon'] ?? '');
|
||||||
|
$items = trim($_POST['items']);
|
||||||
|
$position = (int) ($_POST['position'] ?? 0);
|
||||||
|
|
||||||
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
|
$stmt = $dbh->prepare('
|
||||||
|
UPDATE skills
|
||||||
|
SET title = :title, icon = :icon, items = :items, position = :position
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':icon', $icon, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':items', $items, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':position', $position, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
} 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;
|
||||||
+65
-2
@@ -1,8 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
// On démarre la session
|
// On démarre la session
|
||||||
session_start();
|
require_once(__DIR__ . '/../functions/security.php');
|
||||||
// On ajoute la DB
|
// On ajoute la DB
|
||||||
require_once('../functions/databaseConnection.php');
|
require_once('../functions/databaseConnection.php');
|
||||||
|
// On ajoute les logs Discord
|
||||||
|
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
|
// Si l'utilisateur n'est pas admin il ne peut pas executer le reste du script
|
||||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||||
@@ -10,6 +13,15 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification du jeton CSRF : bloque les requêtes forgées depuis un autre site
|
||||||
|
requireCsrfToken();
|
||||||
|
|
||||||
|
// Le fichier envoyé dépasse post_max_size : $_POST/$_FILES sont vides, on le détecte
|
||||||
|
// tout de suite (sinon le script redirige silencieusement vers admin.php sans explication)
|
||||||
|
if (postDepasseLimite()) {
|
||||||
|
die(messageDepassementPost());
|
||||||
|
}
|
||||||
|
|
||||||
// Si le formulaire à un champ vide
|
// Si le formulaire à un champ vide
|
||||||
if (
|
if (
|
||||||
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
!isset($_POST['id']) || !is_numeric($_POST['id']) ||
|
||||||
@@ -26,29 +38,80 @@ if (
|
|||||||
//On récupère les données pour les mettres en variables locales
|
//On récupère les données pour les mettres en variables locales
|
||||||
$id = (int) $_POST['id'];
|
$id = (int) $_POST['id'];
|
||||||
$title = trim($_POST['title']);
|
$title = trim($_POST['title']);
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
$xptype = trim($_POST['xptype']);
|
$xptype = trim($_POST['xptype']);
|
||||||
$tags = trim($_POST['tags'] ?? '');
|
$tags = trim($_POST['tags'] ?? '');
|
||||||
$description = trim($_POST['description']);
|
$description = trim($_POST['description']);
|
||||||
$duration = trim($_POST['duration']);
|
$duration = trim($_POST['duration']);
|
||||||
$imageurl = trim($_POST['imageurl']);
|
$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
|
try { // On essaie d'executer une requête qui est préparée
|
||||||
|
// Le PDF (pdf_data) n'est plus stocké dans experiences : il vit dans experience_pdfs
|
||||||
|
// (table associée, voir migration Juillet 2026 (6)), géré à part juste après
|
||||||
$stmt = $dbh->prepare('
|
$stmt = $dbh->prepare('
|
||||||
UPDATE experiences
|
UPDATE experiences
|
||||||
SET title = :title, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url
|
SET title = :title, status = :status, xptype = :xptype, tags = :tags, description = :description, duration = :duration, image_url = :image_url, markdown = :markdown, link_url = :link_url
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
');
|
');
|
||||||
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
$stmt->bindValue(':xptype', $xptype, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
$stmt->bindValue(':tags', $tags, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
$stmt->bindValue(':duration', $duration, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
$stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':markdown', $markdown, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':link_url', $link_url, PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$stmt->execute(); // Sinon on renvoie l'erreur
|
$stmt->execute(); // Sinon on renvoie l'erreur
|
||||||
|
|
||||||
|
if ($updatePdf) {
|
||||||
|
if ($pdfData !== null) {
|
||||||
|
// Nouveau PDF envoyé : on remplace l'éventuel PDF existant (clé primaire = experience_id)
|
||||||
|
$stmtPdf = $dbh->prepare('
|
||||||
|
INSERT INTO experience_pdfs (experience_id, pdf_filename, pdf_data) VALUES (:id, :pdf_filename, :pdf_data)
|
||||||
|
ON DUPLICATE KEY UPDATE pdf_filename = VALUES(pdf_filename), pdf_data = VALUES(pdf_data)
|
||||||
|
');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->bindValue(':pdf_filename', $pdfFilename, PDO::PARAM_STR);
|
||||||
|
$stmtPdf->bindValue(':pdf_data', $pdfData, PDO::PARAM_LOB);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
} else {
|
||||||
|
// Case "Supprimer ce PDF" cochée, sans nouveau fichier envoyé
|
||||||
|
$stmtPdf = $dbh->prepare('DELETE FROM experience_pdfs WHERE experience_id = :id');
|
||||||
|
$stmtPdf->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
|
$stmtPdf->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erreur lors de la modification de l experience :" . $e->getMessage());
|
die("Erreur lors de la modification de l experience :" . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log Discord
|
||||||
|
sendLog("Modification de l'expérience : " . $title . " (id " . $id . ")");
|
||||||
|
|
||||||
// On renvoie l'utilisateur sur admin.php
|
// On renvoie l'utilisateur sur admin.php
|
||||||
header('Location: ../pages/admin.php');
|
header('Location: ../pages/admin.php');
|
||||||
exit;
|
exit;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?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
|
||||||
|
// (le PDF est stocké dans une table associée depuis la migration Juillet 2026 (6),
|
||||||
|
// pour ne plus jamais charger son contenu binaire lors des listes de projets/xp/écoles)
|
||||||
|
$tables = [
|
||||||
|
'projet' => ['pdf' => 'project_pdfs', 'colonne' => 'project_id'],
|
||||||
|
'experience' => ['pdf' => 'experience_pdfs', 'colonne' => 'experience_id'],
|
||||||
|
'ecole' => ['pdf' => 'school_pdfs', 'colonne' => 'school_id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$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]['pdf'] . '` WHERE `' . $tables[$type]['colonne'] . '` = :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;
|
||||||
@@ -0,0 +1,677 @@
|
|||||||
|
body {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top, rgba(255, 122, 26, 0.16), transparent 35%),
|
||||||
|
#0a0a10;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
|
||||||
|
color: white;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-shell {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-card,
|
||||||
|
.success-screen {
|
||||||
|
background: linear-gradient(180deg, #111116 0%, #161622 100%);
|
||||||
|
border: 1px solid #262638;
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
|
||||||
|
padding: 30px;
|
||||||
|
animation: cardEnter 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cardEnter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 122, 26, 0.14);
|
||||||
|
border: 1px solid rgba(255, 122, 26, 0.3);
|
||||||
|
color: #ffa155;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-title {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
color: #c1c1cf;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-subtitle strong {
|
||||||
|
color: #ffa155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-target {
|
||||||
|
min-width: 240px;
|
||||||
|
background-color: #1a1a24;
|
||||||
|
border: 1px solid #262638;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-target span {
|
||||||
|
display: block;
|
||||||
|
color: #8f8fa8;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-target strong {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #ffa155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-attempts {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
background-color: #171b28;
|
||||||
|
border: 1px solid #283049;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-attempts span {
|
||||||
|
display: block;
|
||||||
|
color: #99a0bc;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-attempts strong {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pastilles visuelles représentant les essais restants */
|
||||||
|
.attempts-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attempts-dots span {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attempts-dots .dot-filled {
|
||||||
|
background: linear-gradient(180deg, #ff7a1a 0%, #e8630a 100%);
|
||||||
|
box-shadow: 0 0 8px rgba(255, 122, 26, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attempts-dots .dot-empty {
|
||||||
|
background-color: #2a2f42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-reset,
|
||||||
|
.captcha-secondary-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
background-color: #2a2a3a;
|
||||||
|
border: 1px solid #3a3a4e;
|
||||||
|
transition: transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-reset:hover,
|
||||||
|
.captcha-secondary-button:hover {
|
||||||
|
background-color: #34344a;
|
||||||
|
border-color: rgba(255, 122, 26, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-error {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
background-color: rgba(161, 44, 47, 0.15);
|
||||||
|
border: 1px solid rgba(255, 84, 84, 0.45);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #ffb8bb;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
animation: errorShake 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes errorShake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
20% { transform: translateX(-6px); }
|
||||||
|
40% { transform: translateX(5px); }
|
||||||
|
60% { transform: translateX(-4px); }
|
||||||
|
80% { transform: translateX(3px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox {
|
||||||
|
background-color: #0f1118;
|
||||||
|
border: 1px solid #22263a;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: #141825;
|
||||||
|
border-bottom: 1px solid #22263a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-dots span {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-dots span:nth-child(1) {
|
||||||
|
background-color: #ff5f57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-dots span:nth-child(2) {
|
||||||
|
background-color: #febc2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-dots span:nth-child(3) {
|
||||||
|
background-color: #28c840;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-label {
|
||||||
|
color: #d6daf0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-option {
|
||||||
|
position: relative;
|
||||||
|
opacity: 0;
|
||||||
|
animation: mailEnter 0.4s ease forwards;
|
||||||
|
animation-delay: calc(var(--mail-index, 0) * 0.04s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mailEnter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-option-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-option input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anneau de focus visible au clavier (accessibilité) sur la ligne associée à la checkbox */
|
||||||
|
.mail-option input:focus-visible + .mail-row {
|
||||||
|
outline: 2px solid #ff7a1a;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 26px 48px minmax(120px, 220px) 1fr 70px;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-bottom: 1px solid #1d2234;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-row:hover {
|
||||||
|
background-color: #161b2a;
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-option input:checked + .mail-row {
|
||||||
|
background-color: rgba(255, 122, 26, 0.12);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 122, 26, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case à cocher personnalisée */
|
||||||
|
.mail-check {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #1a1a24;
|
||||||
|
border: 1px solid #3a3a4e;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-option input:checked + .mail-row .mail-check {
|
||||||
|
background: linear-gradient(180deg, #ff7a1a 0%, #e8630a 100%);
|
||||||
|
border-color: #ff7a1a;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-avatar {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(180deg, #2a2a3a 0%, #1a1a24 100%);
|
||||||
|
border: 1px solid #303040;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #d7dcf2;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-sender {
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-subject {
|
||||||
|
color: #f3f4fb;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-preview {
|
||||||
|
color: #b5b8ca;
|
||||||
|
font-size: 13px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-time {
|
||||||
|
color: #8f8fa8;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Champ piège (honeypot) : invisible pour un humain, exclu du tabulateur,
|
||||||
|
mais toujours présent dans le HTML pour les robots qui remplissent tout */
|
||||||
|
.captcha-honeypot {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-note {
|
||||||
|
color: #b7bbcd;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-submit {
|
||||||
|
background: linear-gradient(180deg, #ff8c3a 0%, #e8630a 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 24px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
box-shadow: 0 12px 24px rgba(232, 99, 10, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-submit:hover {
|
||||||
|
transform: translateY(-1px) scale(1.01);
|
||||||
|
box-shadow: 0 16px 30px rgba(232, 99, 10, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-submit:active {
|
||||||
|
transform: translateY(1px) scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-mode {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-screen {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: successPop 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
font-size: 68px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
animation: bounceIn 0.9s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-screen h1 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-screen p {
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
max-width: 520px;
|
||||||
|
color: #ced2e6;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bloc pédagogique listant les indices de phishing repérés */
|
||||||
|
.success-reasons {
|
||||||
|
max-width: 460px;
|
||||||
|
margin: 18px auto 26px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
text-align: left;
|
||||||
|
background: rgba(255, 122, 26, 0.08);
|
||||||
|
border: 1px solid rgba(255, 122, 26, 0.25);
|
||||||
|
border-radius: 16px;
|
||||||
|
animation: cardEnter 0.5s ease 0.3s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-reasons-title {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #ffa155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-reasons ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-reasons li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
color: #d8d9e6;
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
margin-top: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff8c3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-loader {
|
||||||
|
width: 240px;
|
||||||
|
height: 10px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #23283d;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #323a5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-loader span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
background: linear-gradient(90deg, #ffa155 0%, #ff7a1a 100%);
|
||||||
|
animation: loadingBar 2.4s linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-burst {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 30%, rgba(255, 197, 92, 0.25), transparent 18%),
|
||||||
|
radial-gradient(circle at 80% 20%, rgba(98, 210, 111, 0.2), transparent 18%),
|
||||||
|
radial-gradient(circle at 30% 80%, rgba(255, 122, 26, 0.22), transparent 20%),
|
||||||
|
radial-gradient(circle at 75% 70%, rgba(255, 145, 77, 0.18), transparent 18%);
|
||||||
|
animation: burstPulse 1s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes successPop {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounceIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.3) translateY(20px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.08) translateY(-6px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadingBar {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes burstPulse {
|
||||||
|
from {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.captcha-card,
|
||||||
|
.success-screen,
|
||||||
|
.mail-option,
|
||||||
|
.success-reasons,
|
||||||
|
.success-icon,
|
||||||
|
.captcha-error {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-burst {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.captcha-top {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-target {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-row {
|
||||||
|
grid-template-columns: 22px 42px 1fr 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-sender {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-submit,
|
||||||
|
.captcha-secondary-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
body {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-card,
|
||||||
|
.success-screen {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-toolbar {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-row {
|
||||||
|
grid-template-columns: 20px 36px 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-time {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-screen h1 {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-screen p {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-loader {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2095
-176
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user