diff --git a/README.md b/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/cms_simplifie/.env b/cms_simplifie/.env new file mode 100644 index 0000000..f174621 --- /dev/null +++ b/cms_simplifie/.env @@ -0,0 +1,4 @@ +DB_HOST=db +DB_NAME=cms_simplifie +DB_USER=cms +DB_PASS=cmspass diff --git a/cms_simplifie/.htaccess b/cms_simplifie/.htaccess new file mode 100644 index 0000000..957aeb4 --- /dev/null +++ b/cms_simplifie/.htaccess @@ -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 + + Require all denied + + +# 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) + + Require all denied + + + + + # 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'" + + + + + +# Redirection sécurisé: racine -> /public +RewriteCond %{REQUEST_URI} ^/$ +RewriteRule ^$ public/ [R=302,L] + diff --git a/cms_simplifie/Dockerfile b/cms_simplifie/Dockerfile new file mode 100644 index 0000000..8be96a5 --- /dev/null +++ b/cms_simplifie/Dockerfile @@ -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 diff --git a/cms_simplifie/README_DOCKER.md b/cms_simplifie/README_DOCKER.md new file mode 100644 index 0000000..c824b07 --- /dev/null +++ b/cms_simplifie/README_DOCKER.md @@ -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 \ No newline at end of file diff --git a/cms_simplifie/admin/articles_create.php b/cms_simplifie/admin/articles_create.php new file mode 100644 index 0000000..854b200 --- /dev/null +++ b/cms_simplifie/admin/articles_create.php @@ -0,0 +1,76 @@ +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'; +?> + +

Écrire un article

+ + + +
+ +

+ +
+ + + +
+ + +
+ + +
+ +
+ + +
+ + + + Annuler +
+ + diff --git a/cms_simplifie/admin/articles_delete.php b/cms_simplifie/admin/articles_delete.php new file mode 100644 index 0000000..1d7e009 --- /dev/null +++ b/cms_simplifie/admin/articles_delete.php @@ -0,0 +1,60 @@ +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 "

Accès refusé.

"; + 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'; +?> +

Supprimer l'article

+
+ +

Supprimer ?

+ + +
+ + + +
+ + + Annuler +
+ diff --git a/cms_simplifie/admin/articles_edit.php b/cms_simplifie/admin/articles_edit.php new file mode 100644 index 0000000..0e0ff83 --- /dev/null +++ b/cms_simplifie/admin/articles_edit.php @@ -0,0 +1,82 @@ +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 "

Accès refusé.

"; + 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'; +?> +

Modifier l'article #

+ + +
+ +

+ +
+ + +
+ + +
+ + + +
+ +
+ + + +
+ + + + Annuler +
+ + + diff --git a/cms_simplifie/admin/dashboard.php b/cms_simplifie/admin/dashboard.php new file mode 100644 index 0000000..45492e0 --- /dev/null +++ b/cms_simplifie/admin/dashboard.php @@ -0,0 +1,82 @@ +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(); +} +?> + + +

Mon espace

+
Bienvenue,
+

Vos articles :

+ +

+ + Écrire un nouvel article + + Retourner a l'accueil +

+ + + + + + + + + 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): ?> + + + + + + + + + +
IDTitreDateAuteurActions
+ + Modifier + Supprimer +
+ + diff --git a/cms_simplifie/admin/login.php b/cms_simplifie/admin/login.php new file mode 100644 index 0000000..f508a51 --- /dev/null +++ b/cms_simplifie/admin/login.php @@ -0,0 +1,77 @@ +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'; +?> +

Connexion

+ + +
+ +

+ +
+ + + +
+ + +
+ + + +
+ +
+ + +
+ + + Créer un compte +
+ + diff --git a/cms_simplifie/admin/logout.php b/cms_simplifie/admin/logout.php new file mode 100644 index 0000000..2d33e8e --- /dev/null +++ b/cms_simplifie/admin/logout.php @@ -0,0 +1,5 @@ +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'; +?> +

Inscription

+ + + +
+ +

+ +
+ + + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + J'ai déjà un compte +
+ + diff --git a/cms_simplifie/assets/css/style.css b/cms_simplifie/assets/css/style.css new file mode 100644 index 0000000..eabbaa1 --- /dev/null +++ b/cms_simplifie/assets/css/style.css @@ -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} diff --git a/cms_simplifie/docker-compose.yml b/cms_simplifie/docker-compose.yml new file mode 100644 index 0000000..6f40c63 --- /dev/null +++ b/cms_simplifie/docker-compose.yml @@ -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: diff --git a/cms_simplifie/inc/config.php b/cms_simplifie/inc/config.php new file mode 100644 index 0000000..2b55889 --- /dev/null +++ b/cms_simplifie/inc/config.php @@ -0,0 +1,11 @@ + 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'); + diff --git a/cms_simplifie/inc/db.php b/cms_simplifie/inc/db.php new file mode 100644 index 0000000..c893043 --- /dev/null +++ b/cms_simplifie/inc/db.php @@ -0,0 +1,12 @@ +PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC + ]); +} catch(PDOException $e) { + http_response_code(500); + echo "

Erreur DB

".htmlspecialchars($e->getMessage())."

"; + exit; +} diff --git a/cms_simplifie/inc/footer.php b/cms_simplifie/inc/footer.php new file mode 100644 index 0000000..77a9128 --- /dev/null +++ b/cms_simplifie/inc/footer.php @@ -0,0 +1,9 @@ + + + + diff --git a/cms_simplifie/inc/functions.php b/cms_simplifie/inc/functions.php new file mode 100644 index 0000000..0834a0a --- /dev/null +++ b/cms_simplifie/inc/functions.php @@ -0,0 +1,45 @@ +'; } + +// 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 "

Requête invalide

Jeton CSRF manquant ou invalide.

"; + exit; + } + } +} diff --git a/cms_simplifie/inc/header.php b/cms_simplifie/inc/header.php new file mode 100644 index 0000000..d5f41e9 --- /dev/null +++ b/cms_simplifie/inc/header.php @@ -0,0 +1,28 @@ + + + + + + + <?= e(APP_NAME) ?> + + + + +
diff --git a/cms_simplifie/index.php b/cms_simplifie/index.php new file mode 100644 index 0000000..a160bb9 --- /dev/null +++ b/cms_simplifie/index.php @@ -0,0 +1,4 @@ +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; +} +?> +
+ +

+ + +

+ Par + — +

+ + +
+
+ + +

← Retour à l'accueil

+ + diff --git a/cms_simplifie/public/author.php b/cms_simplifie/public/author.php new file mode 100644 index 0000000..372266f --- /dev/null +++ b/cms_simplifie/public/author.php @@ -0,0 +1,50 @@ +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 "

Utilisateur introuvable.

"; + require_once __DIR__ . '/../inc/footer.php'; + exit; +} + +// Titre de page avec le login +echo '

Post de ' . e($user['login']) . '

'; + +// 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 '
'; + echo '

'.e($a['titre']).'

'; + echo '

'.e(date('d/m/Y H:i', strtotime($a['date_creation']))).'

'; + echo '

'.e(excerpt($a['contenu'])).'

'; + echo '

Lire la suite

'; + echo '
'; +} + +// Footer +require_once __DIR__ . '/../inc/footer.php'; diff --git a/cms_simplifie/public/index.php b/cms_simplifie/public/index.php new file mode 100644 index 0000000..047f277 --- /dev/null +++ b/cms_simplifie/public/index.php @@ -0,0 +1,43 @@ +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 +?> +

Derniers posts

+ + + +

Aucun article pour l'instant.

+ + + +
+ +

+ + +

+ Par + — +

+ + +

+ + +

Lire la suite

+
+ + + diff --git a/cms_simplifie/public/page404.php b/cms_simplifie/public/page404.php new file mode 100644 index 0000000..2eea343 --- /dev/null +++ b/cms_simplifie/public/page404.php @@ -0,0 +1,5 @@ +
+

Erreur 404

+

La page demandée est introuvable.

+

Revenir à l'accueil

+
diff --git a/cms_simplifie/sql/dump.sql b/cms_simplifie/sql/dump.sql new file mode 100644 index 0000000..9ecf3a2 --- /dev/null +++ b/cms_simplifie/sql/dump.sql @@ -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');