ajout projet cms elyes

This commit is contained in:
elyes
2025-11-03 00:39:36 +01:00
parent a1f7262e89
commit daa719eac7
25 changed files with 924 additions and 0 deletions

4
cms_simplifie/.env Normal file
View File

@@ -0,0 +1,4 @@
DB_HOST=db
DB_NAME=cms_simplifie
DB_USER=cms
DB_PASS=cmspass

63
cms_simplifie/.htaccess Normal file
View File

@@ -0,0 +1,63 @@
# ===================================================================
# Sécurité de base
# ===================================================================
ServerSignature Off
Options -Indexes
FileETag None
AddDefaultCharset UTF-8
# Bloque fichiers cachés (.env, .git, etc.) et configs/dev
<FilesMatch "^(\.|composer\.(json|lock)|package(-lock)?\.json|Dockerfile|docker-compose\.yml|.*\.env|.*\.sql|.*\.md)$">
Require all denied
</FilesMatch>
# Forcer une page 404 propre
ErrorDocument 404 /page404.php
# Empêche l'accès direct aux dossiers non publics
RewriteEngine On
RewriteRule ^inc/ - [F,L,NC]
RewriteRule ^sql/ - [F,L,NC]
# Méthodes HTTP autorisées (GET/POST/HEAD)
<LimitExcept GET POST HEAD>
Require all denied
</LimitExcept>
<IfModule mod_headers.c>
# Empêche le framing (clickjacking)
Header always set X-Frame-Options "SAMEORIGIN"
# Empêche le MIME sniffing
Header always set X-Content-Type-Options "nosniff"
# Politique du Referer
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Permissions API (désactive tout par défaut)
Header always set Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), usb=(), interest-cohort=()"
# Politique de ressources cross-origin
Header always set Cross-Origin-Resource-Policy "same-origin"
Header always set Cross-Origin-Opener-Policy "same-origin"
# Content Security Policy (CSP)
# - autorise CSS en ligne
# - pas d'exec JS hormis le fichier local .js (je n'ai pas utilisé de code javascript donc le site ne devrait jamais utiliser de code js )
Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; img-src 'self' data:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'"
</IfModule>
# Redirection sécurisé: racine -> /public
RewriteCond %{REQUEST_URI} ^/$
RewriteRule ^$ public/ [R=302,L]

7
cms_simplifie/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM php:8.2-apache
RUN docker-php-ext-install pdo pdo_mysql
RUN a2enmod rewrite
RUN apt-get update && apt-get install -y tzdata
WORKDIR /var/www/html
COPY . /var/www/html
RUN chown -R www-data:www-data /var/www/html

View File

@@ -0,0 +1,19 @@
# CMS Simplifié
## Pour lancer
docker compose up -d --build
- Site : http://localhost:8080/public/index.php
## Compte admin
Identifiant :admin
Mdp :VH515f6frrv11e651
- meme le mot de passe de l'admin est haché
## Notes
- PHP + Apache + mysql avec requete preparer
- Sécurité basique : CSRF sur POST, password_hash/verify, PDO préparé
- Mise en place des sécurités pour eviter les attaques par XSS grace a la fonction e (échapper)
- Uniquement le dossier public est exposé : mise en place des sécurité par le fichier .htaccess
- Pas d'execution javascript en ligne ou insérer malicieusement. seul le code dans le fichier.js peut etre executer mais j'en ai pas donc le site ne devrait recevoir de code js

View File

@@ -0,0 +1,76 @@
<?php
// Inclusion des fichiers de config, BDD et fonctions
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/functions.php';
// Vérifie que l'utilisateur est connecté, sinon redirection
require_login();
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Vérifie la validité du jeton CSRF pour éviter les attaques
verify_csrf();
// Récupération et nettoyage des champs du formulaire
$titre = trim($_POST['titre'] ?? '');
$contenu = trim($_POST['contenu'] ?? '');
// Vérifie si les champs obligatoires sont remplis
if ($titre === '' || $contenu === '') {
$errors[] = "Tous les champs sont obligatoires.";
}
// Si pas d'erreurs, insertion de l'article dans la base
if (!$errors) {
$stmt = $pdo->prepare("INSERT INTO articles (user_id, titre, contenu, date_creation) VALUES (:uid, :titre, :contenu, NOW())");
$stmt->execute([
'uid'=>current_user_id(), // ID de l'auteur connecté
'titre'=>$titre,
'contenu'=>$contenu
]);
// Redirection vers le tableau de bord après publication
header('Location: /admin/dashboard.php');
exit;
}
}
// Inclusion du header
require_once __DIR__ . '/../inc/header.php';
?>
<h2>Écrire un article</h2>
<!-- Affiche les erreurs éventuelles -->
<?php if ($errors): ?>
<div class="error">
<?php foreach ($errors as $e): ?>
<p><?= e($e) ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Formulaire de création d'article -->
<form method="post">
<?= csrf_input() ?> <!-- Jeton CSRF caché -->
<div class="form-group">
<label>Titre</label>
<input type="text" name="titre" value="<?= e($_POST['titre'] ?? '') ?>">
</div>
<div class="form-group">
<label>Contenu</label>
<textarea name="contenu"><?= e($_POST['contenu'] ?? '') ?></textarea>
</div>
<!-- Bouton publier et annuler -->
<button class="btn primary" type="submit">Publier</button>
<a class="btn" href="/admin/dashboard.php">Annuler</a>
</form>
<?php
// Inclusion du footer
require_once __DIR__ . '/../inc/footer.php';
?>

View File

@@ -0,0 +1,60 @@
<?php
// Charge la config, la connexion BDD et les fonctions
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/functions.php';
// Accès réservé aux utilisateurs connectés
require_login();
// Récupère l'ID de l'article depuis l'URL et le valide
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) { header('Location: /admin/dashboard.php'); exit; }
// Charge les infos de l'article (pour vérifier droits + afficher le titre)
$stmt = $pdo->prepare("SELECT id, user_id, titre FROM articles WHERE id = :id");
$stmt->execute(['id'=>$id]);
$article = $stmt->fetch();
if (!$article) { header('Location: /admin/dashboard.php'); exit; }
// Contrôle d'autorisation : admin ou auteur du post
if (!is_admin() && (int)$article['user_id'] !== current_user_id()) {
http_response_code(403); // interdit
echo "<div class='card error'><p>Accès refusé.</p></div>";
exit;
}
// Traitement du formulaire de confirmation (méthode POST + CSRF)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf(); // vérifie le jeton CSRF
// Si l'utilisateur confirme, on supprime l'article
if (isset($_POST['confirm']) && $_POST['confirm'] === 'yes') {
$del = $pdo->prepare("DELETE FROM articles WHERE id = :id");
$del->execute(['id'=>$id]);
}
// Retour au tableau de bord dans tous les cas
header('Location: /admin/dashboard.php');
exit;
}
// Affichage
require_once __DIR__ . '/../inc/header.php';
?>
<h2>Supprimer l'article</h2>
<div class="card">
<!-- Rappel du titre pour confirmation -->
<p>Supprimer <strong><?= e($article['titre']) ?></strong> ?</p>
<!-- Formulaire de confirmation (protégé CSRF) -->
<form method="post" style="display:inline">
<?= csrf_input() ?> <!-- jeton CSRF -->
<input type="hidden" name="confirm" value="yes">
<button class="btn danger" type="submit">Oui, supprimer</button>
</form>
<!-- Lien d'annulation -->
<a class="btn" href="/admin/dashboard.php">Annuler</a>
</div>
<?php require_once __DIR__ . '/../inc/footer.php'; ?>

View File

@@ -0,0 +1,82 @@
<?php
// Charge la configuration, la connexion BDD et les fonctions
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/functions.php';
// Restreint l'accès : uniquement pour utilisateurs connectés
require_login();
// Récupère et valide l'ID de l'article passé en GET
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) { header('Location: /admin/dashboard.php'); exit; }
// Récupère l'article ciblé
$stmt = $pdo->prepare("SELECT id, user_id, titre, contenu FROM articles WHERE id = :id");
$stmt->execute(['id'=>$id]);
$article = $stmt->fetch();
if (!$article) { header('Location: /admin/dashboard.php'); exit; }
// Vérifie les droits : admin ou auteur de l'article
if (!is_admin() && (int)$article['user_id'] !== current_user_id()) {
http_response_code(403); // interdit
echo "<div class='card error'><p>Accès refusé.</p></div>";
exit;
}
// Gestion du formulaire d'édition
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf(); // protège contre les attaques CSRF
// Nettoyage et validation des champs
$titre = trim($_POST['titre'] ?? '');
$contenu = trim($_POST['contenu'] ?? '');
if ($titre === '' || $contenu === '') { $errors[] = "Tous les champs sont obligatoires."; }
// Si ok, mise à jour de l'article en BDD
if (!$errors) {
$stmt = $pdo->prepare("UPDATE articles SET titre=:titre, contenu=:contenu WHERE id=:id");
$stmt->execute(['titre'=>$titre,'contenu'=>$contenu,'id'=>$id]);
// Retour au tableau de bord après sauvegarde
header('Location: /admin/dashboard.php');
exit;
}
}
// Affichage de la page (header + formulaire + footer)
require_once __DIR__ . '/../inc/header.php';
?>
<h2>Modifier l'article #<?= (int)$article['id'] ?></h2>
<?php if ($errors): ?>
<div class="error">
<?php foreach ($errors as $e): ?>
<p><?= e($e) ?></p> <!-- messages d'erreur échappés -->
<?php endforeach; ?>
</div>
<?php endif; ?>
<form method="post">
<?= csrf_input() ?> <!-- champ caché CSRF -->
<div class="form-group">
<label>Titre</label>
<!-- Pré-remplit avec la valeur existante -->
<input type="text" name="titre" value="<?= e($_POST['titre'] ?? $article['titre']) ?>">
</div>
<div class="form-group">
<label>Contenu</label>
<!-- Pré-remplit avec la saisie en cours ou la valeur existante -->
<textarea name="contenu"><?= e($_POST['contenu'] ?? $article['contenu']) ?></textarea>
</div>
<!-- Actions -->
<button class="btn primary" type="submit">Enregistrer</button>
<a class="btn" href="/admin/dashboard.php">Annuler</a>
</form>
<?php require_once __DIR__ . '/../inc/footer.php'; ?>

View File

@@ -0,0 +1,82 @@
<?php
// Charge la config, la connexion BDD et les fonctions
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/functions.php';
// Restreint l'accès au tableau de bord aux utilisateurs connectés
require_login();
// Affiche le header
require_once __DIR__ . '/../inc/header.php';
// Calcule le nombre d'articles : tous si admin, sinon seulement ceux de l'utilisateur
if (is_admin()) {
$countArticles = (int)$pdo->query("SELECT COUNT(*) FROM articles")->fetchColumn();
} else {
$stmtCount = $pdo->prepare("SELECT COUNT(*) FROM articles WHERE user_id = :uid");
$stmtCount->execute(['uid'=>current_user_id()]);
$countArticles = (int)$stmtCount->fetchColumn();
}
?>
<h2>Mon espace</h2>
<div class="notice">Bienvenue, <?= e(current_user_login()) ?> </div>
<p>Vos articles : <strong><?= $countArticles ?></strong></p>
<p>
<!-- Lien pour créer un nouvel article -->
<a class="btn primary" href="/admin/articles_create.php">Écrire un nouvel article</a>
<!-- Lien pour revenir à la partie publique du site -->
<a class="btn" href="/public/index.php">Retourner a l'accueil</a>
</p>
<table class="table">
<thead>
<tr>
<th>ID</th><th>Titre</th><th>Date</th><th>Auteur</th><th>Actions</th>
</tr>
</thead>
<tbody>
<?php
// Récupère la liste d'articles à afficher : tous si admin, sinon filtrés par user_id
if (is_admin()) {
$stmt = $pdo->query("
SELECT a.id, a.titre, a.date_creation, u.login AS auteur
FROM articles a
LEFT JOIN utilisateur u ON u.id = a.user_id
ORDER BY a.date_creation DESC
");
} else {
$stmt = $pdo->prepare("
SELECT a.id, a.titre, a.date_creation, u.login AS auteur
FROM articles a
LEFT JOIN utilisateur u ON u.id = a.user_id
WHERE a.user_id = :uid
ORDER BY a.date_creation DESC
");
$stmt->execute(['uid'=>current_user_id()]);
}
// Affiche chaque article dans une ligne du tableau (échappé pour éviter les attaques par xss)
foreach ($stmt as $row): ?>
<tr>
<td><?= (int)$row['id'] ?></td>
<td><?= e($row['titre']) ?></td>
<td><?= e(date('d/m/Y H:i', strtotime($row['date_creation']))) ?></td>
<td><?= e($row['auteur'] ?? 'Anonyme') ?></td>
<td>
<!-- Actions d'édition/suppression sur l'article courant -->
<a class="btn" href="/admin/articles_edit.php?id=<?= (int)$row['id'] ?>">Modifier</a>
<a class="btn danger" href="/admin/articles_delete.php?id=<?= (int)$row['id'] ?>">Supprimer</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
// Footer
require_once __DIR__ . '/../inc/footer.php';
?>

View File

@@ -0,0 +1,77 @@
<?php
// Charge la config, la connexion BDD et les fonctions
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/functions.php';
$errors = []; // Contiendra les messages d'erreur à afficher
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf(); // Sécurise le formulaire contre les attaques CSRF
// Récupère et nettoie les identifiants saisis
$loginOrEmail = trim($_POST['login'] ?? '');
$password = $_POST['password'] ?? '';
// Validation des champs requis
if ($loginOrEmail === '' || $password === '') {
$errors[] = "Veuillez renseigner vos identifiants.";
} else {
// Recherche de l'utilisateur par login ou email (requête préparée sécurisée)
$stmt = $pdo->prepare("SELECT id, login, password, role FROM utilisateur WHERE login = :v OR email = :v LIMIT 1");
$stmt->execute(['v'=>$loginOrEmail]);
$user = $stmt->fetch();
// Vérifie le mot de passe
if ($user && password_verify($password, $user['password'])) {
// Sauvegarde l'état de connexion en session
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['login'] = $user['login'];
$_SESSION['role'] = $user['role'] ?: 'user';
// Redirige vers le tableau de bord une fois connecté
header('Location: /admin/dashboard.php'); exit;
} else {
// Message générique pour ne pas révéler si login/email existe
$errors[] = "Identifiants incorrects.";
}
}
}
// header
require_once __DIR__ . '/../inc/header.php';
?>
<h2>Connexion</h2>
<?php if ($errors): ?>
<div class="error">
<?php foreach ($errors as $e): ?>
<p><?= e($e) ?></p> <!-- Affiche les erreurs de manière échappée -->
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Formulaire de connexion protégé par CSRF -->
<form method="post">
<?= csrf_input() ?> <!-- Champ caché contenant le jeton CSRF -->
<div class="form-group">
<label>Login ou Email</label>
<!-- Conserve la saisie utilisateur en cas d'erreur -->
<input type="text" name="login" value="<?= e($_POST['login'] ?? '') ?>">
</div>
<div class="form-group">
<label>Mot de passe</label>
<input type="password" name="password">
</div>
<button class="btn primary" type="submit">Se connecter</button>
<a class="btn" href="/admin/register.php">Créer un compte</a>
</form>
<?php
// footer
require_once __DIR__ . '/../inc/footer.php';
?>

View File

@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/../inc/functions.php';
session_destroy();
header('Location: /public/index.php');
exit;

View File

@@ -0,0 +1,95 @@
<?php
// Charge la config, la connexion BDD et les fonction
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/functions.php';
$errors = []; // Collecte des messages d'erreur
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf(); // Protection CSRF du formulaire
// Récupération et on vide les champs
$login = trim($_POST['login'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$password2 = $_POST['password2'] ?? '';
// Validations de base
if ($login === '' || $email === '' || $password === '' || $password2 === '') { $errors[] = "Tous les champs sont obligatoires."; }
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = "Email invalide."; }
if ($password !== $password2) { $errors[] = "Les mots de passe ne correspondent pas."; }
// Si valide, vérifie login/email puis crée l'utilisateur
if (!$errors) {
$stmt = $pdo->prepare("SELECT id FROM utilisateur WHERE login = :login OR email = :email LIMIT 1");
$stmt->execute(['login'=>$login,'email'=>$email]);
if ($stmt->fetch()) {
$errors[] = "Login ou email déjà pris."; // Conflit
} else {
// Hash sécurisé du mot de passe
$hash = password_hash($password, PASSWORD_DEFAULT);
// Insertion de l'utilisateur en base (rôle par défaut: user)
$ins = $pdo->prepare("INSERT INTO utilisateur (login, email, password, role, created_at) VALUES (:login, :email, :password, 'user', NOW())");
$ins->execute(['login'=>$login,'email'=>$email,'password'=>$hash]);
// Connexion automatique après inscription
$_SESSION['user_id'] = (int)$pdo->lastInsertId();
$_SESSION['login'] = $login;
$_SESSION['role'] = 'user';
// Redirection vers le tableau de bord
header('Location: /admin/dashboard.php'); exit;
}
}
}
// Header
require_once __DIR__ . '/../inc/header.php';
?>
<h2>Inscription</h2>
<!-- Affichage des erreurs éventuelles -->
<?php if ($errors): ?>
<div class="error">
<?php foreach ($errors as $e): ?>
<p><?= e($e) ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Formulaire d'inscription protégé par CSRF -->
<form method="post">
<?= csrf_input() ?> <!-- Jeton CSRF -->
<div class="form-group">
<label>Login</label>
<input type="text" name="login" value="<?= e($_POST['login'] ?? '') ?>">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" value="<?= e($_POST['email'] ?? '') ?>">
</div>
<div class="form-group">
<label>Mot de passe</label>
<input type="password" name="password">
</div>
<div class="form-group">
<label>Confirmer le mot de passe</label>
<input type="password" name="password2">
</div>
<button class="btn primary" type="submit">Créer mon compte</button>
<a class="btn" href="/admin/login.php">J'ai déjà un compte</a>
</form>
<?php
// Footer
require_once __DIR__ . '/../inc/footer.php';
?>

View File

@@ -0,0 +1,22 @@
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,'Helvetica Neue',Arial,'Noto Sans',sans-serif;line-height:1.6;background:#f7f7fb;color:#222}
a{color:#0a66c2;text-decoration:none}
a:hover{text-decoration:underline}
.container{max-width:980px;margin:0 auto;padding:1rem}
.site-header{background:#fff;border-bottom:1px solid #e5e7eb;position:sticky;top:0;z-index:10}
.logo a{color:#111;text-decoration:none}
.site-header nav a{margin-right:1rem}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem;margin:1rem 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}
.card h2{margin-top:.2rem}
.btn{display:inline-block;padding:.6rem 1rem;border-radius:10px;border:1px solid #e5e7eb;background:#fff;cursor:pointer}
.btn.primary{background:#0a66c2;color:#fff;border-color:#0a66c2}
.btn.danger{background:#e11d48;color:#fff;border-color:#e11d48}
.form-group{margin-bottom:1rem}
input[type=text],input[type=password],input[type=email],textarea{width:100%;padding:.6rem;border:1px solid #d1d5db;border-radius:10px;background:#fff}
textarea{min-height:180px}
.table{width:100%;border-collapse:collapse}
.table th,.table td{border:1px solid #e5e7eb;padding:.6rem;text-align:left}
.table th{background:#fafafa}
.error,.notice{padding:.8rem 1rem;border-radius:10px;margin:1rem 0}
.error{background:#fff1f2;border:1px solid #f43f5e}
.notice{background:#ecfeff;border:1px solid #06b6d4}

View File

@@ -0,0 +1,44 @@
services:
app:
build: .
container_name: cms_app
ports:
- "8080:80"
environment:
DB_HOST: db
DB_NAME: cms_simplifie
DB_USER: cms
DB_PASS: cmspass
TZ: Europe/Paris
volumes:
- ./:/var/www/html
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.4
container_name: cms_db
restart: always
environment:
MYSQL_DATABASE: cms_simplifie
MYSQL_USER: cms
MYSQL_PASSWORD: cmspass
MYSQL_ROOT_PASSWORD: rootpass
TZ: Europe/Paris
volumes:
- db_data:/var/lib/mysql
- ./sql/dump.sql:/docker-entrypoint-initdb.d/00_dump.sql:ro
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -prootpass || exit 1"]
interval: 5s
timeout: 7s
retries: 12
volumes:
db_data:

View File

@@ -0,0 +1,11 @@
<?php
$env = fn($k, $d=null) => getenv($k) !== false ? getenv($k) : $d;
define('DB_HOST', $env('DB_HOST', 'localhost'));
define('DB_NAME', $env('DB_NAME', 'cms_simplifie'));
define('DB_USER', $env('DB_USER', 'root'));
define('DB_PASS', $env('DB_PASS', ''));
define('BASE_URL', '');
define('APP_NAME', 'CMS Simplifié');
date_default_timezone_set('Europe/Paris');

12
cms_simplifie/inc/db.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC
]);
} catch(PDOException $e) {
http_response_code(500);
echo "<h1>Erreur DB</h1><p>".htmlspecialchars($e->getMessage())."</p>";
exit;
}

View File

@@ -0,0 +1,9 @@
</main>
<footer class="site-footer">
<div class="container">
<p>&copy; <?= date('Y') ?> <?= e(APP_NAME) ?> — <a href="https://www.basilis-digitale.com" target="_blank" rel="noopener noreferrer">
Développé par Elyes Atoui</a></p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,45 @@
<?php
// Démarre la session si elle n'est pas déjà active
if (session_status() === PHP_SESSION_NONE) { session_start(); }
// Vrai si un utilisateur est connecté
function is_logged_in(): bool { return !empty($_SESSION['user_id']); }
// Renvoie l'ID de l'utilisateur connecté (ou null)
function current_user_id(): ?int { return $_SESSION['user_id'] ?? null; }
// Renvoie le login/pseudo de l'utilisateur (ou null)
function current_user_login(): ?string { return $_SESSION['login'] ?? null; }
// Renvoie le rôle courant (par défaut 'user')
function current_user_role(): string { return $_SESSION['role'] ?? 'user'; }
// Vrai si l'utilisateur courant est admin
function is_admin(): bool { return current_user_role() === 'admin'; }
// Protège une page : redirige vers /admin/login.php si non connecté
function require_login(): void { if (!is_logged_in()) { header('Location: /admin/login.php'); exit; } }
// Échappe le HTML pour éviter les attaque xss
function e(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
// Retourne un extrait limité à 150 caractères
function excerpt(string $c, int $l=150): string { $c=strip_tags($c); return mb_strlen($c)<= $l? $c: mb_substr($c,0,$l).'…'; }
// Génère/récupère le jeton CSRF stocké en session
function csrf_token(): string { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token']=bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; }
// Champ caché à insérer dans les formulaires avec le jeton CSRF
function csrf_input(): string { return '<input type="hidden" name="csrf" value="'.e(csrf_token()).'">'; }
// Vérifie le jeton CSRF sur les requêtes POST, sinon bloque avec 400
function verify_csrf(): void {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$t = $_POST['csrf'] ?? '';
if (!$t || !hash_equals($_SESSION['csrf_token'] ?? '', $t)) {
http_response_code(400);
echo "<h1>Requête invalide</h1><p>Jeton CSRF manquant ou invalide.</p>";
exit;
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/functions.php';
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= e(APP_NAME) ?></title>
<link rel="stylesheet" href="/assets/css/style.css">
</head>
<body>
<header class="site-header">
<div class="container">
<h1 class="logo"><a href="/public/index.php"><?= e(APP_NAME) ?></a></h1>
<nav>
<a href="/public/index.php">Accueil</a>
<?php if (is_logged_in()): ?>
<a href="/admin/dashboard.php">Mon espace</a>
<a href="/admin/logout.php">Déconnexion (<?= e(current_user_login()) ?>)</a>
<?php else: ?>
<a href="/admin/login.php">Connexion</a>
<a href="/admin/register.php">Inscription</a>
<?php endif; ?>
</nav>
</div>
</header>
<main class="container">

4
cms_simplifie/index.php Normal file
View File

@@ -0,0 +1,4 @@
<?php
// Redirige la racine vers la zone publique de l'application
header('Location: /public/index.php', true, 302);
exit;

View File

@@ -0,0 +1,54 @@
<?php
// Charge la config, la connexion BDD et le header
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/header.php';
// Récupère et valide l'ID d'article depuis l'URL
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
http_response_code(404);
include __DIR__.'/page404.php';
require_once __DIR__.'/../inc/footer.php';
exit;
}
// Requête sécurisée : récupère l'article + auteur avec jointure
$stmt = $pdo->prepare("
SELECT a.id, a.titre, a.contenu, a.date_creation, u.login AS auteur
FROM articles a
LEFT JOIN utilisateur u ON u.id = a.user_id
WHERE a.id = :id
");
$stmt->execute(['id'=>$id]);
$article = $stmt->fetch();
// Si aucun article trouvé → 404 propre
if (!$article) {
http_response_code(404);
include __DIR__.'/page404.php';
require_once __DIR__.'/../inc/footer.php';
exit;
}
?>
<article class="card">
<!-- Titre échappé pour prévenir la XSS -->
<h2><?= e($article['titre']) ?></h2>
<!-- Métadonnées : auteur (fallback Anonyme) + date formatée -->
<p class="muted">
Par <a href="/public/author.php?login=<?= urlencode($article['auteur'] ?? 'inconnu') ?>"><?= e($article['auteur'] ?? 'Anonyme') ?></a>
— <?= e(date('d/m/Y H:i', strtotime($article['date_creation']))) ?>
</p>
<!-- Contenu : échappé puis nl2br pour conserver les retours à la ligne -->
<div><?= nl2br(e($article['contenu'])) ?></div>
</article>
<!-- Lien de retour à l'accueil -->
<p><a href="/public/index.php">&larr; Retour à l'accueil</a></p>
<?php
// Footer commun (scripts, fermeture des balises)
require_once __DIR__ . '/../inc/footer.php';
?>

View File

@@ -0,0 +1,50 @@
<?php
// Charge la config, la connexion BDD et le header
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/header.php';
// Récupère le login depuis la requête et le valide
$login = trim($_GET['login'] ?? '');
if ($login === '') {
include __DIR__.'/page404.php';
require_once __DIR__.'/../inc/footer.php';
exit;
}
// Recherche de l'utilisateur par son login (requête préparée)
$stmt = $pdo->prepare("SELECT id, login FROM utilisateur WHERE login = :login");
$stmt->execute(['login'=>$login]);
$user = $stmt->fetch();
// Si l'utilisateur n'existe pas, message simple puis fin
if (!$user) {
echo "<div class='card'><p>Utilisateur introuvable.</p></div>";
require_once __DIR__ . '/../inc/footer.php';
exit;
}
// Titre de page avec le login
echo '<h2>Post de ' . e($user['login']) . '</h2>';
// Récupère les articles de cet auteur (du plus récent au plus ancien)
$stmt = $pdo->prepare("
SELECT id, titre, contenu, date_creation
FROM articles
WHERE user_id = :uid
ORDER BY date_creation DESC
");
$stmt->execute(['uid'=>$user['id']]);
// affichage des articles (extrait + lien "Lire la suite")
foreach ($stmt as $a) {
echo '<article class="card">';
echo '<h2><a href="/public/article.php?id='.(int)$a['id'].'">'.e($a['titre']).'</a></h2>';
echo '<p class="muted">'.e(date('d/m/Y H:i', strtotime($a['date_creation']))).'</p>';
echo '<p>'.e(excerpt($a['contenu'])).'</p>';
echo '<p><a class="btn" href="/public/article.php?id='.(int)$a['id'].'">Lire la suite</a></p>';
echo '</article>';
}
// Footer
require_once __DIR__ . '/../inc/footer.php';

View File

@@ -0,0 +1,43 @@
<?php
// Charge la config, la connexion BDD et le header
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/db.php';
require_once __DIR__ . '/../inc/header.php';
// Récupère les 10 derniers articles avec leur auteur (avec jointure)
$stmt = $pdo->query("
SELECT a.id, a.titre, a.contenu, a.date_creation, u.login AS auteur
FROM articles a
LEFT JOIN utilisateur u ON u.id = a.user_id
ORDER BY a.date_creation DESC
LIMIT 10
");
$articles = $stmt->fetchAll(); // Tableau d'articles pour l'affichage
?>
<h2>Derniers posts</h2>
<?php if (!$articles): ?>
<!-- Message si aucun article n'est disponible -->
<p>Aucun article pour l'instant.</p>
<?php endif; ?>
<?php foreach ($articles as $a): ?>
<article class="card">
<!-- Titre + lien vers la page de l'article -->
<h2><a href="/public/article.php?id=<?= (int)$a['id'] ?>"><?= e($a['titre']) ?></a></h2>
<!-- Métadonnées : auteur + date formatée -->
<p class="muted">
Par <a href="/public/author.php?login=<?= urlencode($a['auteur'] ?? 'inconnu') ?>"><?= e($a['auteur'] ?? 'Anonyme') ?></a>
— <?= e(date('d/m/Y H:i', strtotime($a['date_creation']))) ?>
</p>
<!-- Extrait -->
<p><?= e(excerpt($a['contenu'], 150)) ?></p>
<!-- Lien pour lire l'article complet -->
<p><a class="btn" href="/public/article.php?id=<?= (int)$a['id'] ?>">Lire la suite</a></p>
</article>
<?php endforeach; ?>
<?php require_once __DIR__ . '/../inc/footer.php'; ?>

View File

@@ -0,0 +1,5 @@
<div class="card error">
<h2>Erreur 404</h2>
<p>La page demandée est introuvable.</p>
<p><a href="/public/index.php">Revenir à l'accueil</a></p>
</div>

View File

@@ -0,0 +1,27 @@
-- MySQL 8.x schema
DROP TABLE IF EXISTS articles;
DROP TABLE IF EXISTS utilisateur;
CREATE TABLE utilisateur (
id INT AUTO_INCREMENT PRIMARY KEY,
login VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(190) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('admin','user') NOT NULL DEFAULT 'user',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
titre VARCHAR(255) NOT NULL,
contenu TEXT NOT NULL,
date_creation DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_articles_user FOREIGN KEY (user_id) REFERENCES utilisateur(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO articles (titre, contenu) VALUES
('Bienvenue', 'Bienvenue sur notre CMS !'),
('Ecrire', 'Inscrivez-vous, connectez-vous et redigez vos propres posts.');
INSERT INTO utilisateur (login, email, password, role) VALUES ('admin','admin@example.com','$2y$10$rZeIxDeW.MM/BWDySZxAmuhd6X/6YHfbpawNMqQjdgAJbHEXmS6Ay', 'admin');