386 lines
17 KiB
PHP
386 lines
17 KiB
PHP
<?php
|
|
ini_set('display_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
session_set_cookie_params([
|
|
'secure' => true,
|
|
'httponly' => true,
|
|
'samesite' => 'Strict'
|
|
]);
|
|
session_start();
|
|
|
|
// ========================
|
|
// CONFIGURATION
|
|
// ========================
|
|
require_once __DIR__ . '/config/db.php';
|
|
require_once __DIR__ . '/config/env.php';
|
|
require_once __DIR__ . '/lib/RateLimit.php';
|
|
require_once __DIR__ . '/lib/FileValidator.php';
|
|
|
|
$rateLimit = new RateLimit($pdo);
|
|
$fileValidator = new FileValidator();
|
|
$message = '';
|
|
|
|
// Initialiser le token CSRF
|
|
if (empty($_SESSION['csrf_token'])) {
|
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
}
|
|
|
|
// ========================
|
|
// INSCRIPTION
|
|
// ========================
|
|
if (isset($_POST['register'])) {
|
|
// Vérifier CSRF
|
|
if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
|
|
$message = 'Erreur de sécurité (CSRF)';
|
|
} else {
|
|
$username = trim($_POST['registerUsername'] ?? '');
|
|
$password = $_POST['registerPassword'] ?? '';
|
|
|
|
if (strlen($username) < 3 || strlen($username) > 50) {
|
|
$message = 'Le pseudo doit faire entre 3 et 50 caractères';
|
|
} elseif (strlen($password) < 8) {
|
|
$message = 'Le mot de passe doit faire au moins 8 caractères';
|
|
} elseif (!preg_match('/^[a-zA-Z0-9_-]+$/', $username)) {
|
|
$message = 'Le pseudo ne peut contenir que des lettres, chiffres, - et _';
|
|
} else {
|
|
// Vérifier si le pseudo existe déjà
|
|
$stmt = $pdo->prepare("SELECT id FROM `{$env['TABLE_USERS']}` WHERE pseudo = ?");
|
|
$stmt->execute([$username]);
|
|
|
|
if ($stmt->fetch()) {
|
|
$message = 'Pseudo déjà utilisé';
|
|
} else {
|
|
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
|
|
$stmt = $pdo->prepare(
|
|
"INSERT INTO `{$env['TABLE_USERS']}`
|
|
(pseudo, mot_de_passe, role, date_inscription)
|
|
VALUES (?, ?, 'user', NOW())"
|
|
);
|
|
$stmt->execute([$username, $hash]);
|
|
$message = 'Inscription réussie ! Connectez-vous.';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================
|
|
// CONNEXION
|
|
// ========================
|
|
if (isset($_POST['login'])) {
|
|
// Vérifier CSRF
|
|
if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
|
|
$message = 'Erreur de sécurité (CSRF)';
|
|
} else {
|
|
$username = trim($_POST['loginUsername'] ?? '');
|
|
$password = $_POST['loginPassword'] ?? '';
|
|
$clientIp = $_SERVER['REMOTE_ADDR'];
|
|
|
|
// Rate limiting
|
|
if ($rateLimit->isBlocked($clientIp, 'login', 5, 900)) {
|
|
$message = 'Trop de tentatives. Réessayez dans 15 minutes.';
|
|
} else {
|
|
$stmt = $pdo->prepare(
|
|
"SELECT * FROM `{$env['TABLE_USERS']}` WHERE pseudo = ?"
|
|
);
|
|
$stmt->execute([$username]);
|
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$user || !password_verify($password, $user['mot_de_passe'])) {
|
|
$rateLimit->recordAttempt($clientIp, 'login');
|
|
$message = 'Identifiants incorrects';
|
|
} else {
|
|
$_SESSION['user_id'] = $user['id'];
|
|
$_SESSION['username'] = $user['pseudo'];
|
|
$_SESSION['role'] = $user['role'];
|
|
$message = 'Connexion réussie !';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================
|
|
// DÉCONNEXION
|
|
// ========================
|
|
if (isset($_POST['logout'])) {
|
|
// Vérifier CSRF
|
|
if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
|
|
$message = 'Erreur de sécurité (CSRF)';
|
|
} else {
|
|
$_SESSION = [];
|
|
session_destroy();
|
|
header("Location: " . $_SERVER['PHP_SELF']);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// ========================
|
|
// CRÉATION DE POST
|
|
// ========================
|
|
if (isset($_POST['createPost'])) {
|
|
// Vérifier CSRF
|
|
if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
|
|
$message = 'Erreur de sécurité (CSRF)';
|
|
} elseif (!isset($_SESSION['user_id'])) {
|
|
$message = 'Vous devez être connecté pour poster';
|
|
} else {
|
|
$userId = $_SESSION['user_id'];
|
|
$userIp = $_SERVER['REMOTE_ADDR'];
|
|
|
|
// Rate limiting : 1 post par minute
|
|
if ($rateLimit->isBlocked($userId, 'post', 1, 60)) {
|
|
$message = 'Vous ne pouvez poster qu\'une fois par minute. Patientez...';
|
|
} else {
|
|
$content = trim($_POST['postContent'] ?? '');
|
|
|
|
if (!$content) {
|
|
$message = 'Le contenu du post est vide';
|
|
} elseif (strlen($content) > 5000) {
|
|
$message = 'Le post ne doit pas dépasser 5000 caractères';
|
|
} else {
|
|
// Insérer le post
|
|
$stmt = $pdo->prepare(
|
|
"INSERT INTO `{$env['TABLE_MESSAGES']}`
|
|
(id_utilisateur, contenu, ip_address, date_creation)
|
|
VALUES (?, ?, ?, NOW())"
|
|
);
|
|
$stmt->execute([$userId, $content, $userIp]);
|
|
$postId = $pdo->lastInsertId();
|
|
|
|
// Gérer les fichiers
|
|
if (!empty($_FILES['postImage']['tmp_name'])) {
|
|
$file = $_FILES['postImage'];
|
|
$validationResult = $fileValidator->validate($file, 100 * 1024 * 1024);
|
|
|
|
if ($validationResult['valid']) {
|
|
// Vérifier la sécurité du fichier
|
|
if ($fileValidator->isSafe($file['tmp_name'])) {
|
|
// Créer le répertoire uploads s'il n'existe pas
|
|
$uploadDir = __DIR__ . '/uploads/';
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0755, true);
|
|
}
|
|
|
|
// Générer un nom de fichier sécurisé
|
|
$fileName = uniqid('post_', true) . '.' . $validationResult['ext'];
|
|
$filePath = $uploadDir . $fileName;
|
|
|
|
// Vérifier les permissions
|
|
if (is_writable($uploadDir)) {
|
|
// Déplacer le fichier
|
|
if (move_uploaded_file($file['tmp_name'], $filePath)) {
|
|
// Sauvegarder dans la DB
|
|
$stmt = $pdo->prepare(
|
|
"INSERT INTO `{$env['TABLE_FILES']}`
|
|
(id_message, nom_fichier, chemin_fichier, taille, type_mime, date_upload)
|
|
VALUES (?, ?, ?, ?, ?, NOW())"
|
|
);
|
|
$stmt->execute([
|
|
$postId,
|
|
$file['name'],
|
|
'/uploads/' . $fileName,
|
|
$file['size'],
|
|
$validationResult['mime']
|
|
]);
|
|
$message = 'Post publié avec succès !';
|
|
} else {
|
|
$message = 'Erreur lors de l\'upload';
|
|
}
|
|
} else {
|
|
$message = 'Erreur lors du téléchargement du fichier';
|
|
}
|
|
} else {
|
|
$message = 'Le fichier contient du contenu dangereux';
|
|
}
|
|
} else {
|
|
$message = $validationResult['error'];
|
|
}
|
|
} else {
|
|
$message = 'Post publié avec succès !';
|
|
}
|
|
|
|
// Enregistrer l'activité
|
|
$rateLimit->recordAttempt($userId, 'post');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================
|
|
// CHARGEMENT DES POSTS
|
|
// ========================
|
|
$stmt = $pdo->query(
|
|
"SELECT m.*, u.pseudo, f.chemin_fichier, f.nom_fichier, f.taille
|
|
FROM `{$env['TABLE_MESSAGES']}` m
|
|
JOIN `{$env['TABLE_USERS']}` u ON m.id_utilisateur = u.id
|
|
LEFT JOIN `{$env['TABLE_FILES']}` f ON m.id = f.id_message
|
|
ORDER BY m.date_creation DESC"
|
|
);
|
|
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Si l'utilisateur est admin, charger le flag
|
|
$flag = null;
|
|
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
|
|
$flag = $env['FLAG'];
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Forum Équipe J</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
|
|
.container { max-width: 800px; margin: 0 auto; }
|
|
.header { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); text-align: center; margin-bottom: 30px; }
|
|
.header h1 { color: #333; margin-bottom: 10px; }
|
|
.header p { color: #666; margin-bottom: 10px; }
|
|
.flag-btn { background: #ffc107; color: #333; padding: 12px 25px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; margin-top: 10px; }
|
|
.flag-btn:hover { background: #ffb300; } .flag-box { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-bottom: 20px; border-radius: 5px; display: none; }
|
|
.flag-box.show { display: block; }
|
|
.flag-box strong { color: #856404; }
|
|
.auth-section { background: white; padding: 25px; border-radius: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); margin-bottom: 30px; }
|
|
.auth-section h2 { color: #333; margin-bottom: 15px; font-size: 18px; }
|
|
.form-group { margin-bottom: 15px; }
|
|
.form-group label { display: block; margin-bottom: 5px; color: #555; font-weight: bold; }
|
|
.form-group input, .form-group textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
|
|
.form-group textarea { resize: vertical; min-height: 100px; }
|
|
.btn { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; margin-right: 10px; }
|
|
.btn:hover { background: #5568d3; }
|
|
.btn-danger { background: #dc3545; }
|
|
.btn-danger:hover { background: #c82333; }
|
|
.message { padding: 15px; border-radius: 5px; margin-bottom: 20px; text-align: center; font-weight: bold; }
|
|
.message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
|
.message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
|
.post { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
|
.post-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; }
|
|
.post-author { color: #667eea; font-weight: bold; }
|
|
.post-date { color: #999; font-size: 12px; }
|
|
.post-content { color: #333; line-height: 1.6; margin-bottom: 15px; }
|
|
.post-image { max-width: 100%; border-radius: 5px; margin-top: 10px; }
|
|
.no-posts { text-align: center; color: #999; padding: 40px; }
|
|
.user-status { color: #667eea; font-weight: bold; margin-bottom: 20px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🚀 Forum Équipe J</h1>
|
|
<p>Espace de discussion sécurisé</p>
|
|
<a href="#flag" class="flag-btn" onclick="toggleFlag(event)">📌 Accéder au Flag</a>
|
|
</div>
|
|
|
|
<?php if ($flag): ?>
|
|
<div class="flag-box" id="flag">
|
|
<strong>🏁 FLAG : <?php echo htmlspecialchars($flag, ENT_QUOTES, 'UTF-8'); ?></strong>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="flag-box" id="flag">
|
|
<strong>🔒 Vous devez être connecté en tant qu'admin pour voir le flag</strong>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($message): ?>
|
|
<div class="message <?php echo (strpos($message, 'réussie') !== false || strpos($message, 'succès') !== false) ? 'success' : 'error'; ?>">
|
|
<?php echo htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (isset($_SESSION['user_id'])): ?>
|
|
<div class="user-status">
|
|
👤 Connecté en tant que : <?php echo htmlspecialchars($_SESSION['username'], ENT_QUOTES, 'UTF-8'); ?>
|
|
<?php if ($_SESSION['role'] === 'admin'): ?>
|
|
<span style="color: #ffc107;">[ADMIN]</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="auth-section">
|
|
<h2>✍️ Créer un post</h2>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
|
<div class="form-group">
|
|
<label>Commentaire:</label>
|
|
<textarea name="postContent" required></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Image (PNG ou JPEG, max 100Mo):</label>
|
|
<input type="file" name="postImage" accept="image/png,image/jpeg">
|
|
</div>
|
|
<button type="submit" name="createPost" class="btn">📤 Publier</button>
|
|
<button type="submit" name="logout" class="btn btn-danger">🚪 Déconnexion</button>
|
|
</form>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="auth-section">
|
|
<h2>Connexion</h2>
|
|
<form method="POST">
|
|
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
|
<div class="form-group">
|
|
<label>Pseudo:</label>
|
|
<input type="text" name="loginUsername" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Mot de passe:</label>
|
|
<input type="password" name="loginPassword" required>
|
|
</div>
|
|
<button type="submit" name="login" class="btn">Se connecter</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="auth-section">
|
|
<h2>Inscription</h2>
|
|
<form method="POST">
|
|
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
|
<div class="form-group">
|
|
<label>Pseudo:</label>
|
|
<input type="text" name="registerUsername" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Mot de passe:</label>
|
|
<input type="password" name="registerPassword" required>
|
|
</div>
|
|
<button type="submit" name="register" class="btn">S'inscrire</button>
|
|
</form>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<h2 style="color: white; margin-top: 30px; margin-bottom: 20px;">💬 Commentaires récents</h2>
|
|
<?php if (empty($posts)): ?>
|
|
<div class="no-posts">📭 Aucun post pour le moment. Soyez le premier à poster !</div>
|
|
<?php else: ?>
|
|
<?php foreach ($posts as $post): ?>
|
|
<div class="post">
|
|
<div class="post-header">
|
|
<span class="post-author">👤 <?php echo htmlspecialchars($post['pseudo'], ENT_QUOTES, 'UTF-8'); ?></span>
|
|
<span class="post-date"><?php echo htmlspecialchars($post['date_creation'], ENT_QUOTES, 'UTF-8'); ?> - IP: <?php echo htmlspecialchars($post['ip_address'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); ?></span>
|
|
</div>
|
|
<div class="post-content">
|
|
<?php echo nl2br(htmlspecialchars($post['contenu'], ENT_QUOTES, 'UTF-8')); ?>
|
|
</div>
|
|
<?php if ($post['chemin_fichier']): ?>
|
|
<img src="<?php echo htmlspecialchars($post['chemin_fichier'], ENT_QUOTES, 'UTF-8'); ?>" alt="Post image" class="post-image">
|
|
<small style="color: #999;">📎 <?php echo htmlspecialchars($post['nom_fichier'], ENT_QUOTES, 'UTF-8'); ?> (<?php echo round($post['taille'] / 1024 / 1024, 2); ?> Mo)</small>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleFlag(event) {
|
|
event.preventDefault();
|
|
const flagBox = document.getElementById('flag');
|
|
flagBox.classList.toggle('show');
|
|
|
|
const btn = document.querySelector('.flag-btn');
|
|
if (flagBox.classList.contains('show')) {
|
|
btn.textContent = '📌 Masquer le Flag';
|
|
} else {
|
|
btn.textContent = '📌 Afficher le Flag';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|