| Server IP : 46.105.57.169 / Your IP : 216.73.217.35 Web Server : Apache System : Linux webm002.cluster120.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64 User : verseaumee ( 152031) PHP Version : 8.5.7 Disable Function : _dyuweyrj4,_dyuweyrj4r,dl MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : OFF | Pkexec : OFF Directory : /home/verseaumee/quaadraa/app/Controllers/ |
Upload File : |
<?php
namespace App\Controllers;
use App\Core\Lang;
use App\Core\Security;
use App\Models\Database;
use App\Models\SubsidiaryModel;
class AdminController
{
public function __construct(private Lang $lang) {}
private function guard(): void
{
if (session_status() === PHP_SESSION_NONE) session_start();
if (empty($_SESSION['admin_id'])) {
header('Location: /admin/login');
exit;
}
$timeout = 1800;
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $timeout)) {
session_unset();
session_destroy();
Security::log('SESSION_EXPIRED', 'Inactivité détectée pour admin_id: ' . $_SESSION['admin_id']);
header('Location: /admin/login?expired=1');
exit;
}
$_SESSION['last_activity'] = time();
$sessId = $_SESSION['admin_session_id'] ?? '';
$row = Database::fetchOne(
'SELECT * FROM admin_sessions WHERE id = :id AND admin_id = :aid AND expires_at > NOW()',
[':id' => $sessId, ':aid' => $_SESSION['admin_id']]
);
if (!$row ||
$row['ip_address'] !== ($_SERVER['REMOTE_ADDR'] ?? '') ||
$row['user_agent'] !== ($_SERVER['HTTP_USER_AGENT'] ?? '')) {
Security::log('SECURITY_BREACH', 'Tentative de session invalide. IP: ' . ($_SERVER['REMOTE_ADDR'] ?? 'n/a'));
session_destroy();
header('Location: /admin/login?expired=1');
exit;
}
}
private function checkAccess(array $allowedRoles): void
{
if (session_status() === PHP_SESSION_NONE) session_start();
if (!isset($_SESSION['admin_id'])) {
header('Location: /admin/login');
exit;
}
$admin = \App\Models\Database::fetchOne('SELECT is_active FROM admins WHERE id = ?', [$_SESSION['admin_id']]);
if (!$admin || (int)$admin['is_active'] === 0) {
session_destroy();
header('Location: /admin/login');
exit;
}
if ($_SESSION['admin_id'] == 1) {
return;
}
if (!in_array($_SESSION['admin_role'] ?? '', $allowedRoles)) {
header('Location: /admin?error=unauthorized');
exit;
}
}
private function renderAdmin(string $view, array $data = []): void
{
$data['adminLang'] = $this->lang;
$data['e'] = fn(mixed $v) => Security::e($v);
$data['csrf'] = Security::csrfToken();
$data['admin'] = $_SESSION['admin_username'] ?? 'Admin';
extract($data, EXTR_SKIP);
$viewFile = dirname(__DIR__, 2) . "/admin/views/{$view}.php";
if (!file_exists($viewFile)) {
die("Admin view not found: {$view}");
}
require dirname(__DIR__, 2) . '/admin/views/layout.php';
}
public function users(): void
{
$this->checkAccess(['superadmin']);
$this->renderAdmin('users', [
'pageTitle' => 'Gestion des utilisateurs',
'admins' => Database::fetchAll('SELECT * FROM admins'),
'activeMenu' => 'users',
'csrf' => \App\Core\Security::csrfToken()
]);
}
public function userCreate(): void
{
$this->checkAccess(['superadmin']);
Security::verifyCsrf();
Database::insert('INSERT INTO admins (username, email, password_hash, role, created_at) VALUES (:u, :e, :p, :r, NOW())', [
':u' => $_POST['username'], ':e' => $_POST['email'], ':p' => password_hash($_POST['password'], PASSWORD_DEFAULT), ':r' => $_POST['role']
]);
header('Location: /admin/users'); exit;
}
public function userToggle(array $params): void
{
$this->checkAccess(['superadmin']);
Security::verifyCsrf();
if ((int)$params['id'] !== (int)$_SESSION['admin_id']) {
Database::execute('UPDATE admins SET is_active = NOT is_active WHERE id = :id', [':id' => $params['id']]);
}
header('Location: /admin/users'); exit;
}
public function userDelete(array $params): void
{
$this->checkAccess(['superadmin']);
Security::verifyCsrf();
if ((int)$params['id'] !== (int)$_SESSION['admin_id']) {
Database::execute('DELETE FROM admins WHERE id = :id', [':id' => $params['id']]);
}
header('Location: /admin/users'); exit;
}
public function loginForm(array $params = []): void
{
if (session_status() === PHP_SESSION_NONE) session_start();
if (!empty($_SESSION['admin_id'])) {
header('Location: /admin');
exit;
}
$expired = isset($_GET['expired']);
require dirname(__DIR__, 2) . '/admin/views/login.php';
}
public function loginSubmit(array $params = []): void
{
if (session_status() === PHP_SESSION_NONE) session_start();
Security::verifyCsrf();
if (!Security::rateLimit('admin_login_' . $_SERVER['REMOTE_ADDR'], 5, 300)) {
$error = 'Trop de tentatives. Réessayez dans 5 minutes.';
require dirname(__DIR__, 2) . '/admin/views/login.php';
return;
}
$email = Security::sanitizeEmail($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$admin = Database::fetchOne(
'SELECT * FROM admins WHERE email = :email AND is_active = 1',
[':email' => $email]
);
if (!$admin || !Security::verifyPassword($password, $admin['password_hash'])) {
$error = 'Identifiants incorrects.';
require dirname(__DIR__, 2) . '/admin/views/login.php';
return;
}
session_regenerate_id(true);
$sessionId = bin2hex(random_bytes(64));
$expiresAt = date('Y-m-d H:i:s', time() + 7200);
Database::insert(
'INSERT INTO admin_sessions (id, admin_id, ip_address, user_agent, expires_at)
VALUES (:id, :aid, :ip, :ua, :exp)',
[
':id' => $sessionId,
':aid' => $admin['id'],
':ip' => $_SERVER['REMOTE_ADDR'] ?? '',
':ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
':exp' => $expiresAt,
]
);
Database::execute(
'UPDATE admins SET last_login = NOW() WHERE id = :id',
[':id' => $admin['id']]
);
$_SESSION['admin_id'] = $admin['id'];
$_SESSION['admin_username'] = $admin['username'];
$_SESSION['admin_role'] = $admin['role'];
$_SESSION['admin_session_id'] = $sessionId;
$_SESSION['last_activity'] = time();
header('Location: /admin');
exit;
}
public function logout(array $params = []): void
{
if (session_status() === PHP_SESSION_NONE) session_start();
if (!empty($_SESSION['admin_session_id'])) {
Database::execute(
'DELETE FROM admin_sessions WHERE id = :id',
[':id' => $_SESSION['admin_session_id']]
);
}
session_destroy();
header('Location: /admin/login');
exit;
}
public function dashboard(array $params = []): void
{
$this->guard();
$stats = [
'news' => Database::fetchOne('SELECT COUNT(*) AS cnt FROM news')['cnt'] ?? 0,
'contacts' => Database::fetchOne('SELECT COUNT(*) AS cnt FROM contacts WHERE is_read = 0')['cnt'] ?? 0,
'subs' => Database::fetchOne('SELECT COUNT(*) AS cnt FROM subsidiaries')['cnt'] ?? 0,
];
$recentContacts = Database::fetchAll(
'SELECT * FROM contacts ORDER BY created_at DESC LIMIT 5'
);
$this->renderAdmin('dashboard', [
'pageTitle' => 'Dashboard',
'stats' => $stats,
'recentContacts' => $recentContacts,
'activeMenu' => 'dashboard',
]);
}
public function pages(array $params = []): void
{
$this->guard();
$pages = Database::fetchAll('SELECT * FROM pages ORDER BY lang, sort_order');
$this->renderAdmin('pages', [
'pageTitle' => 'Gestion des pages',
'pages' => $pages,
'activeMenu' => 'pages',
]);
}
public function pageEdit(array $params = []): void
{
$this->guard();
$id = (int)($params['id'] ?? 0);
$pageData = Database::fetchOne('SELECT * FROM pages WHERE id = :id', [':id' => $id]);
if (!$pageData) {
header('Location: /admin/pages');
exit;
}
$this->renderAdmin('page-edit', [
'pageTitle' => 'Modifier : ' . mb_substr($pageData['title'], 0, 40),
'pageData' => $pageData,
'errors' => [],
'success' => false,
'activeMenu' => 'pages',
]);
}
public function pageUpdate(array $params = []): void
{
$this->guard();
Security::verifyCsrf();
$id = (int)($params['id'] ?? 0);
$pageData = Database::fetchOne('SELECT * FROM pages WHERE id = :id', [':id' => $id]);
if (!$pageData) {
header('Location: /admin/pages');
exit;
}
$errors = [];
$title = trim($_POST['title'] ?? '');
$metaDesc = trim($_POST['meta_desc'] ?? '');
$metaKeywords = trim($_POST['meta_keywords'] ?? '');
$content = $_POST['content'] ?? '';
$sortOrder = max(0, (int)($_POST['sort_order'] ?? 0));
$isPublished = isset($_POST['is_published']) ? 1 : 0;
$heroImagePath = $_POST['current_hero_image'] ?? $pageData['hero_image'];
if (!empty($_FILES['hero_image']['name']) && $_FILES['hero_image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = ROOT_PATH . '/assets/images';
$fileName = Security::secureUpload($_FILES['hero_image'], $uploadDir);
if ($fileName) {
$heroImagePath = '/assets/images/' . $fileName;
} else {
$errors['hero_image'] = 'Format d\'image non autorisé.';
}
}
if (strlen($title) < 2) {
$errors['title'] = 'Le titre est obligatoire (min. 2 caractères).';
}
if (!empty($errors)) {
$this->renderAdmin('page-edit', [
'pageTitle' => 'Modifier : ' . mb_substr($title, 0, 40),
'pageData' => array_merge($pageData, $_POST, ['id' => $id, 'hero_image' => $heroImagePath]),
'errors' => $errors,
'success' => false,
'activeMenu' => 'pages',
]);
return;
}
Database::execute(
'UPDATE pages SET title = :title, meta_desc = :meta_desc, meta_keywords = :meta_keywords, hero_image = :hero_image, content = :content, sort_order = :sort_order, is_published = :is_published, updated_by = :updated_by, updated_at = NOW() WHERE id = :id',
[':title' => $title, ':meta_desc' => $metaDesc, ':meta_keywords' => $metaKeywords, ':hero_image' => $heroImagePath, ':content' => $content, ':sort_order' => $sortOrder, ':is_published' => $isPublished, ':updated_by' => $_SESSION['admin_id'], ':id' => $id]
);
$pageData = Database::fetchOne('SELECT * FROM pages WHERE id = :id', [':id' => $id]);
$this->renderAdmin('page-edit', [
'pageTitle' => 'Modifier : ' . mb_substr($pageData['title'], 0, 40),
'pageData' => $pageData,
'errors' => [],
'success' => true,
'activeMenu' => 'pages',
]);
}
public function subsidiaries(array $params = []): void
{
$this->guard();
$subs = Database::fetchAll('SELECT s.*, st.name FROM subsidiaries s LEFT JOIN subsidiary_translations st ON s.id = st.subsidiary_id WHERE st.lang = "fr"');
$this->renderAdmin('subsidiaries', [
'pageTitle' => 'Gestion des Filiales',
'subs' => $subs,
'activeMenu' => 'subsidiaries',
]);
}
public function subsidiaryEdit(array $params = []): void
{
$this->guard();
$id = (int)($params['id'] ?? 0);
$sub = Database::fetchOne('SELECT * FROM subsidiaries WHERE id = :id', [':id' => $id]);
$translations = Database::fetchAll('SELECT * FROM subsidiary_translations WHERE subsidiary_id = :id', [':id' => $id]);
$data = [];
foreach ($translations as $t) { $data[$t['lang']] = $t; }
$this->renderAdmin('subsidiary-edit', [
'pageTitle' => 'Modifier Filiale',
'id' => $id,
'sub' => $sub,
'translations' => $data,
'activeMenu' => 'subsidiaries',
]);
}
public function subsidiaryDelete(array $params = []): void
{
$this->guard();
Security::verifyCsrf();
$id = (int)($params['id'] ?? 0);
$sub = Database::fetchOne('SELECT image_path FROM subsidiaries WHERE id = ?', [$id]);
if ($sub && !empty($sub['image_path'])) {
$filePath = ROOT_PATH . $sub['image_path'];
if (file_exists($filePath)) unlink($filePath);
}
Database::execute('DELETE FROM subsidiary_translations WHERE subsidiary_id = ?', [$id]);
Database::execute('DELETE FROM subsidiaries WHERE id = ?', [$id]);
header('Location: /admin/subsidiaries?deleted=1');
exit;
}
public function subsidiaryUpdate(array $params = []): void
{
$this->guard();
Security::verifyCsrf();
$id = (int)($params['id'] ?? 0);
$data = $_POST['data'];
foreach ($data as $lang => $values) {
Database::execute('UPDATE subsidiary_translations SET name = :name, tagline = :tagline, description = :description WHERE subsidiary_id = :id AND lang = :lang',
[':name' => $values['name'], ':tagline' => $values['tagline'], ':description' => $values['description'], ':id' => $id, ':lang' => $lang]);
}
if (isset($_FILES['sub_image']) && $_FILES['sub_image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = ROOT_PATH . '/assets/images/activites';
$fileName = Security::secureUpload($_FILES['sub_image'], $uploadDir);
if ($fileName) {
Database::execute('UPDATE subsidiaries SET image_path = ? WHERE id = ?', ['/assets/images/activites/' . $fileName, $id]);
}
}
header('Location: /admin/subsidiaries?success=1');
exit;
}
public function news(array $params = []): void
{
$this->guard();
$news = Database::fetchAll('SELECT n.*, st.name AS sub_name FROM news n LEFT JOIN subsidiaries s ON s.id = n.subsidiary_id LEFT JOIN subsidiary_translations st ON st.subsidiary_id = n.subsidiary_id AND st.lang = "fr" ORDER BY n.created_at DESC');
$this->renderAdmin('news', ['pageTitle' => 'Actualités', 'news' => $news, 'activeMenu' => 'news']);
}
public function newsNew(array $params = []): void
{
$this->guard();
$this->renderAdmin('news-form', ['pageTitle' => 'Nouvelle actualité', 'subs' => SubsidiaryModel::getAll('fr'), 'activeMenu' => 'news', 'article' => null, 'errors' => []]);
}
public function newsCreate(array $params = []): void
{
$this->guard();
Security::verifyCsrf();
$title = trim($_POST['title'] ?? '');
$lang = in_array($_POST['lang'] ?? 'fr', ['fr','en']) ? $_POST['lang'] : 'fr';
$slug = Security::slug($title) . '-' . time();
\App\Models\NewsModel::create([
':slug' => $slug, ':lang' => $lang, ':subsidiary_id' => !empty($_POST['subsidiary_id']) ? (int)$_POST['subsidiary_id'] : null,
':title' => $title, ':excerpt' => trim($_POST['excerpt'] ?? ''), ':content' => $_POST['content'] ?? '',
':cover_image' => null, ':is_published' => isset($_POST['is_published']) ? 1 : 0,
':is_featured' => isset($_POST['is_featured']) ? 1 : 0, ':published_at' => isset($_POST['is_published']) ? date('Y-m-d H:i:s') : null,
':created_by' => $_SESSION['admin_id'],
]);
header('Location: /admin/news?created=1');
exit;
}
public function newsEdit(array $params = []): void
{
$this->guard();
$id = (int)($params['id'] ?? 0);
$article = Database::fetchOne('SELECT * FROM news WHERE id = :id', [':id' => $id]);
if (!$article) { header('Location: /admin/news'); exit; }
$this->renderAdmin('news-edit', ['pageTitle' => 'Modifier', 'article' => $article, 'subs' => SubsidiaryModel::getAll('fr'), 'activeMenu' => 'news']);
}
public function newsUpdate(array $params = []): void
{
$this->guard();
Security::verifyCsrf();
$id = (int)($params['id'] ?? 0);
$article = Database::fetchOne('SELECT * FROM news WHERE id = :id', [':id' => $id]);
$coverImage = $_POST['current_cover_image'] ?? $article['cover_image'];
if (!empty($_FILES['cover_image']['name']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$fileName = Security::secureUpload($_FILES['cover_image'], ROOT_PATH . '/assets/images');
if ($fileName) $coverImage = '/assets/images/' . $fileName;
}
if (!empty($_POST['remove_cover'])) $coverImage = null;
Database::execute('UPDATE news SET title = :t, slug = :s, excerpt = :e, content = :c, cover_image = :img, is_published = :pub, updated_at = NOW() WHERE id = :id',
[':t' => $_POST['title'], ':s' => Security::slug($_POST['slug']), ':e' => $_POST['excerpt'], ':c' => $_POST['content'], ':img' => $coverImage, ':pub' => isset($_POST['is_published']) ? 1 : 0, ':id' => $id]);
header('Location: /admin/news?updated=1');
exit;
}
public function newsDelete(array $params = []): void
{
$this->guard();
Security::verifyCsrf();
Database::execute('DELETE FROM news WHERE id = :id', [':id' => (int)($params['id'] ?? 0)]);
header('Location: /admin/news?deleted=1');
exit;
}
public function contacts(array $params = []): void
{
$this->guard();
$contacts = Database::fetchAll('SELECT * FROM contacts ORDER BY created_at DESC');
Database::execute('UPDATE contacts SET is_read = 1 WHERE is_read = 0');
$this->renderAdmin('contacts', ['pageTitle' => 'Messages', 'contacts' => $contacts, 'activeMenu' => 'contacts']);
}
public function settings(array $params = []): void
{
$this->guard();
$settings = array_column(Database::fetchAll('SELECT * FROM settings'), 'value', 'key');
$this->renderAdmin('settings', ['pageTitle' => 'Paramètres', 'settings' => $settings, 'activeMenu' => 'settings']);
}
public function settingsSave(array $params = []): void
{
$this->guard();
Security::verifyCsrf();
foreach (Database::fetchAll('SELECT `key` FROM settings') as $row) {
if (isset($_POST[$row['key']])) {
Database::execute('UPDATE settings SET value = :val WHERE `key` = :key', [':val' => trim($_POST[$row['key']]), ':key' => $row['key']]);
}
}
header('Location: /admin/settings?saved=1');
exit;
}
}