From 21f34c64c9a42596f8e7ef2f2e10f1537d3ac107 Mon Sep 17 00:00:00 2001 From: Oleg Barykin Date: Wed, 27 May 2026 13:54:59 +0200 Subject: [PATCH] =?UTF-8?q?V10.1=20stable=20avec=20correction=20securit?= =?UTF-8?q?=C3=A9,bug=20sur=20csrf=20dans=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .htaccess | 270 ++++++++++++-- admin/index.php | 216 +++++++----- admin/tools/connecter_en_admin.php | 156 +++++---- admin/tools/retour_admin.php | 28 +- includes/admin_style.css | 195 ++++++++++- includes/avatar_initiales.php | 116 +++--- index.php | 38 +- public/contact_admin.php | 546 +++++++++++++++++++---------- public/lang/fr.php | 5 + public/lang/ru.php | 7 +- result_search.php | 4 +- 11 files changed, 1136 insertions(+), 445 deletions(-) diff --git a/.htaccess b/.htaccess index 5af7133..350430a 100644 --- a/.htaccess +++ b/.htaccess @@ -1,4 +1,227 @@ -# --- Sécurité : en-têtes globaux --- +# BEGIN Security - Shared rules for all sites +# Last updated: 2026-05-22 + +# Block known attacker ranges (VPS/hosting providers used for brute force) + +RewriteEngine On +# FranTech / PonyNET / BuyVM +RewriteCond %{REMOTE_ADDR} ^107\.189\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^209\.141\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^104\.244\.73\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^198\.98\. +RewriteRule .* - [F,L] +# WatchComm / FNI +RewriteCond %{REMOTE_ADDR} ^205\.185\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^45\.61\.18[4-8]\. +RewriteRule .* - [F,L] +# AltunHOST / hosting providers +RewriteCond %{REMOTE_ADDR} ^5\.83\.135\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^192\.109\.200\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^103\.60\.12\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^185\.198\.240\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^173\.239\.240\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^104\.234\.53\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^104\.238\.222\. +RewriteRule .* - [F,L] +# TECHOFF SRV LIMITED (scraper) +RewriteCond %{REMOTE_ADDR} ^195\.178\.110\. +RewriteRule .* - [F,L] +# Brute force attackers (May 11) +RewriteCond %{REMOTE_ADDR} ^103\.59\.161\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^185\.153\.151\. +RewriteRule .* - [F,L] +# F.N.S. HOLDINGS LIMITED +RewriteCond %{REMOTE_ADDR} ^216\.73\.160\. +RewriteRule .* - [F,L] +# DigitalOcean brute force +RewriteCond %{REMOTE_ADDR} ^143\.198\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^146\.190\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^142\.93\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^170\.64\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^134\.209\. +RewriteRule .* - [F,L] +# Cipher Operations (Serbia) +RewriteCond %{REMOTE_ADDR} ^62\.60\.130\. +RewriteRule .* - [F,L] +# UK Dedicated Servers +RewriteCond %{REMOTE_ADDR} ^185\.192\.7[0-1]\. +RewriteRule .* - [F,L] +# GSL Networks +RewriteCond %{REMOTE_ADDR} ^85\.203\.2[1-3]\. +RewriteRule .* - [F,L] +# Argon Data Communication +RewriteCond %{REMOTE_ADDR} ^157\.66\.56\. +RewriteRule .* - [F,L] +# Pfcloud UG +RewriteCond %{REMOTE_ADDR} ^217\.60\.241\. +RewriteRule .* - [F,L] +# Omegatech LTD +RewriteCond %{REMOTE_ADDR} ^158\.94\.210\. +RewriteRule .* - [F,L] +# Individual attackers +RewriteCond %{REMOTE_ADDR} ^94\.26\.106\.111$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^176\.65\.132\.166$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^37\.120\.217\.182$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^8\.210\.170\.233$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^120\.24\.67\.156$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^140\.245\.111\.5$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^20\.9\.82\.33$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^45\.156\.87\.222$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^31\.57\.184\.107$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^92\.205\.109\.21$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^72\.167\.150\.128$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^165\.154\.38\.157$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^50\.6\.207\.27$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^141\.98\.11\.117$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^136\.243\.176\.166$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^123\.30\.233\.1[2-3]$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^91\.227\.37\.60$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^78\.142\.18\.172$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^66\.116\.235\.214$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^149\.88\.110\.49$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^35\.227\.109\.86$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^103\.253\.27\.44$ +RewriteRule .* - [F,L] +# Contabo (individual IP) +RewriteCond %{REMOTE_ADDR} ^85\.239\.235\.108$ +RewriteRule .* - [F,L] +# xmlrpc + brute force attackers (May 22) +RewriteCond %{REMOTE_ADDR} ^105\.184\.79\.191$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^45\.94\.31\.119$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^138\.2\.98\.238$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^20\.89\.64\.143$ +RewriteRule .* - [F,L] +# Vulnerability scanners (May 22-23) +RewriteCond %{REMOTE_ADDR} ^143\.92\.32\.30$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^194\.26\.192\.219$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^38\.99\.81\.38$ +RewriteRule .* - [F,L] +# Massive brute force (May 23) +RewriteCond %{REMOTE_ADDR} ^193\.26\.115\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^51\.79\.177\.162$ +RewriteRule .* - [F,L] +# ALFA shell scanners / 2ipcore crawler +RewriteCond %{REMOTE_ADDR} ^185\.213\.154\.185$ +RewriteRule .* - [F,L] +# Vulnerability scanners (May 23 evening) +# Microsoft Azure — shell/backdoor scanner +RewriteCond %{REMOTE_ADDR} ^20\.151\.117\.104$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^4\.228\.100\.214$ +RewriteRule .* - [F,L] +# 2ipcore ALFA shell scanners (expand to /24) +RewriteCond %{REMOTE_ADDR} ^2\.58\.56\. +RewriteRule .* - [F,L] +# DigitalOcean scanners +RewriteCond %{REMOTE_ADDR} ^157\.230\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^139\.59\. +RewriteRule .* - [F,L] +# Directory enumeration scanner +RewriteCond %{REMOTE_ADDR} ^91\.199\.118\.57$ +RewriteRule .* - [F,L] +# ALFA shell scanners (expand to /24) +RewriteCond %{REMOTE_ADDR} ^45\.92\.1\. +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^45\.154\.98\. +RewriteRule .* - [F,L] +# Google Cloud spammer +RewriteCond %{REMOTE_ADDR} ^34\.42\.34\.237$ +RewriteRule .* - [F,L] +# Azure vulnerability scanners (May 24) +RewriteCond %{REMOTE_ADDR} ^20\.48\.233\.234$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^20\.63\.100\.211$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^20\.226\.23\.23$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^20\.9\.81\.163$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^20\.63\.46\.176$ +RewriteRule .* - [F,L] +# Persistent brute force (Hetzner) +RewriteCond %{REMOTE_ADDR} ^168\.119\.26\.89$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^173\.249\.4\.57$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^216\.251\.35\.201$ +RewriteRule .* - [F,L] +# File enumeration scanner (May 25) +RewriteCond %{REMOTE_ADDR} ^158\.158\.44\.35$ +RewriteRule .* - [F,L] +# Reconnaissance bots / Tor (May 25) +RewriteCond %{REMOTE_ADDR} ^149\.102\.230\.133$ +RewriteRule .* - [F,L] +RewriteCond %{REMOTE_ADDR} ^82\.102\.18\.116$ +RewriteRule .* - [F,L] +# Directory scanner (May 25) +RewriteCond %{REMOTE_ADDR} ^195\.3\.220\.7$ +RewriteRule .* - [F,L] +# Azure shell scanner (May 25) +RewriteCond %{REMOTE_ADDR} ^20\.226\.59\.228$ +RewriteRule .* - [F,L] +# WP installer scanner (May 26) +RewriteCond %{REMOTE_ADDR} ^194\.26\.192\.75$ +RewriteRule .* - [F,L] +# Azure webshell scanner (May 26) +RewriteCond %{REMOTE_ADDR} ^104\.43\.242\.179$ +RewriteRule .* - [F,L] +# Block non-existent plugin/theme probes (saves PHP workers) +RewriteRule ^ALFA_DATA/ - [F,L] +RewriteRule ^alfacgiapi/ - [F,L] + + +# Block xmlrpc.php + + + Require all denied + + +# END Security + +# --- Security headers --- Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "DENY" @@ -6,60 +229,65 @@ Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()" Header always set Cross-Origin-Opener-Policy "same-origin" Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - # CSP en observation (ne bloque rien pour l’instant) + # CSP en observation (ne bloque rien pour l'instant) # --- CSP (global) : autorise Matomo + CDNs --- Header always set Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https: https://staging.uslugi.fr/matomo/; script-src 'self' 'unsafe-inline' https://staging.uslugi.fr/matomo/ https://cdn.jsdelivr.net https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none'" -# Réduire surface d’attaque +# Reduce attack surface Options -Indexes -MultiViews -# --- Réécriture & HTTPS --- +# --- Rewrite & HTTPS --- RewriteEngine On -# Forcer HTTPS (OK derrière proxy/Varnish) +# Force HTTPS (behind proxy/Varnish) RewriteCond %{HTTPS} !=on RewriteCond %{HTTP:X-Forwarded-Proto} !https RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] -# Laisser passer Let's Encrypt +# Let's Encrypt RewriteRule ^\.well-known/acme-challenge/ - [L] -# --- Protection chemins & fichiers sensibles --- -# Bloquer VCS +# --- Sensitive paths & files --- +# Block VCS RewriteRule ^(\.git|\.svn|\.hg)(/|$) - [F] -# Bloquer dotfiles (sauf .well-known) +# Block dotfiles (except .well-known) RewriteRule ^\.(?!well-known/) - [F] -# Bloquer extensions sensibles +# Block sensitive extensions RewriteRule \.(env|ini|log|bak|old|sql|zip|tar|gz|7z)$ - [F] -# Bloquer fichiers sauvegardes *~ +# Block backup files *~ RewriteRule ^(.*/)?[^/]+~$ - [F] -# Bloquer certains noms explicites +# Block specific filenames Require all denied -# Ceinture+bretelles : jamais de scripts dans /uploads +# No scripts in /uploads RewriteRule ^(?:public/)?uploads/.*\.(php|phtml|phar|phps|cgi|pl|py|sh|rb)$ - [F,NC] -# --- Routage applicatif --- -# 0) Servir les fichiers/dirs existants +# --- Application routing --- +# 0) Serve existing files/dirs RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^ - [L] -# 1) (optionnel) Normaliser la barre finale (sauf /) +# 1) Normalize trailing slash (except /) RewriteCond %{REQUEST_URI} !^/$ RewriteCond %{REQUEST_URI} (.+)/$ RewriteRule ^ %1 [R=301,L] -# 2) URL "propres" sans extension -> router.php +# 2) Clean URLs -> router.php RewriteRule ^([^.]+)$ router.php?slug=$1 [L,QSA] -# 3) Fallback général +# 2bis) Unknown PHP files => 404 +RewriteCond %{REQUEST_URI} \.php$ [NC] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^ - [R=404,L] + +# 3) General fallback RewriteRule ^ index.php [L,QSA] -# --- PWA / Types & cache SW --- +# --- PWA / Types & SW cache --- AddType application/manifest+json .webmanifest @@ -68,7 +296,7 @@ AddType application/manifest+json .webmanifest Header set Expires "0" -# Activer des en-têtes différents uniquement pour /visio/join.php +# Visio-specific headers for /visio/join.php SetEnvIf Request_URI "^/visio/join\.php$" IS_VISIO=1 @@ -81,5 +309,3 @@ SetEnvIf Request_URI "^/visio/join\.php$" IS_VISIO=1 DirectoryIndex index.php index.html - - diff --git a/admin/index.php b/admin/index.php index 7a6066c..9e704bf 100644 --- a/admin/index.php +++ b/admin/index.php @@ -1,7 +1,4 @@ - @@ -197,7 +230,37 @@ ?> + diff --git a/admin/tools/connecter_en_admin.php b/admin/tools/connecter_en_admin.php index fd44203..ef84a55 100644 --- a/admin/tools/connecter_en_admin.php +++ b/admin/tools/connecter_en_admin.php @@ -1,49 +1,51 @@
'.htmlspecialchars(($translations['access_denied'] ?? 'Доступ запрещён.'), ENT_QUOTES, 'UTF-8').'
'; + echo '
' . htmlspecialchars(($translations['access_denied'] ?? 'Доступ запрещён.'), ENT_QUOTES, 'UTF-8') . '
'; exit; -} -// reauth (10 min) -$mustReauth=function(int $ttl=600){$t=(int)($_SESSION['admin_reauth_at']??0);return ($t===0||(time()-$t)>$ttl);}; -if ($mustReauth()){ - $qs=http_build_query(['page'=>'reauth','redir'=>($_SERVER['REQUEST_URI']??'index.php?page=maintenance')]); - header('Location:?'.$qs); + } + // reauth (10 min) + $mustReauth = function (int $ttl = 600) {$t = (int) ($_SESSION['admin_reauth_at'] ?? 0);return ($t === 0 || (time() - $t) > $ttl);}; + if ($mustReauth()) { + $qs = http_build_query(['page' => 'reauth', 'redir' => ($_SERVER['REQUEST_URI'] ?? 'index.php?page=maintenance')]); + header('Location:?' . $qs); exit; -} + } -// CSRF -if (!isset($_SESSION['csrf_token'])) { + // CSRF + if (! isset($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); -} -$CSRF = $_SESSION['csrf_token']; + } + $CSRF = $_SESSION['csrf_token']; -function e($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } + function e($s) + {return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');} -// Helpers DB : test existence de la table de log -function tableExists(PDO $pdo, string $table): bool { + // Helpers DB : test existence de la table de log + function tableExists(PDO $pdo, string $table): bool + { $sql = "SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1"; $st = $pdo->prepare($sql); $st->execute([$table]); - return (bool)$st->fetchColumn(); -} + return (bool) $st->fetchColumn(); + } -// ------------ Traitement POST ------------ -$flash = null; + // ------------ Traitement POST ------------ + $flash = null; -if (($_POST['do'] ?? '') === 'impersonate') { + if (($_POST['do'] ?? '') === 'impersonate') { try { - if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) { + if (! hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) { throw new Exception('CSRF'); } - $email = trim((string)($_POST['email'] ?? '')); + $email = trim((string) ($_POST['email'] ?? '')); if ($email === '') { throw new Exception('NO_EMAIL'); } @@ -57,37 +59,37 @@ if (($_POST['do'] ?? '') === 'impersonate') { $st->execute([$email]); $user = $st->fetch(PDO::FETCH_ASSOC); - if (!$user) { + if (! $user) { throw new Exception('NOT_FOUND'); } - $role = (string)($user['role'] ?? ''); + $role = (string) ($user['role'] ?? ''); // On autorise seulement certains rôles à être "impersonnés" $allowedRoles = ['praticien', 'client']; - if (!in_array($role, $allowedRoles, true)) { + if (! in_array($role, $allowedRoles, true)) { throw new Exception('ROLE_FORBIDDEN'); } // Sauvegarder l’admin actuel si ce n’est pas déjà fait - if (empty($_SESSION['admin_impersonate']) || !is_array($_SESSION['admin_impersonate'])) { + if (empty($_SESSION['admin_impersonate']) || ! is_array($_SESSION['admin_impersonate'])) { $_SESSION['admin_impersonate'] = [ - 'admin_user_id' => $_SESSION['user_id'] ?? null, + 'admin_compte_id' => $_SESSION['compte_id'] ?? null, 'admin_reference_id' => $_SESSION['reference_id'] ?? null, - 'admin_role' => $_SESSION['role'] ?? null, + 'admin_role' => $_SESSION['role'] ?? null, 'started_at' => time(), ]; } // Log de l’action si la table existe if (tableExists($pdo, 'admin_impersonation_log')) { - $ip = $_SERVER['REMOTE_ADDR'] ?? null; + $ip = $_SERVER['REMOTE_ADDR'] ?? null; $ua = $_SERVER['HTTP_USER_AGENT'] ?? null; - $adminUid = (int)($_SESSION['user_id'] ?? 0); - $adminRef = (int)($_SESSION['reference_id'] ?? 0); - $targetUid = (int)($user['id'] ?? 0); - $targetRef = (int)($user['reference_id'] ?? 0); - $emailLog = (string)($user['email'] ?? ''); + $adminUid = (int) ($_SESSION['compte_id'] ?? 0); + $adminRef = (int) ($_SESSION['reference_id'] ?? 0); + $targetUid = (int) ($user['id'] ?? 0); + $targetRef = (int) ($user['reference_id'] ?? 0); + $emailLog = (string) ($user['email'] ?? ''); $logSql = "INSERT INTO admin_impersonation_log (admin_user_id, admin_reference_id, target_user_id, target_reference_id, target_role, email, ip, user_agent) @@ -105,15 +107,34 @@ if (($_POST['do'] ?? '') === 'impersonate') { } // Basculer la session sur l’utilisateur cible - $_SESSION['user_id'] = (int)$user['id']; - $_SESSION['role'] = $role; - $_SESSION['reference_id'] = (int)($user['reference_id'] ?? 0); + session_regenerate_id(true); + + unset( + $_SESSION['client_id'], + $_SESSION['praticien_id'], + $_SESSION['secretaire_id'], + $_SESSION['admin_id'], + $_SESSION['compte_id'], + $_SESSION['reference_id'], + $_SESSION['role'] + ); + + $_SESSION['compte_id'] = (int) $user['id']; + $_SESSION['role'] = $role; + $_SESSION['reference_id'] = (int) ($user['reference_id'] ?? 0); + $_SESSION['last_activity'] = time(); + + if ($role === 'praticien') { + $_SESSION['praticien_id'] = (int) $user['reference_id']; + } elseif ($role === 'client') { + $_SESSION['client_id'] = (int) $user['reference_id']; + } // Redirection selon le rôle if ($role === 'praticien') { - header('Location: /public/espace_praticien/index.php'); + header('Location: /public/praticien/agenda.php'); } elseif ($role === 'client') { - header('Location: /public/espace_client/index.php'); + header('Location: /public/client/mes_rendez_vous.php'); } else { // fallback header('Location: /'); @@ -124,56 +145,67 @@ if (($_POST['do'] ?? '') === 'impersonate') { $code = $e->getMessage(); $msg = ($translations['admin_impersonate_err_default'] ?? 'Ошибка при подключении.'); - if ($code === 'CSRF') $msg = ($translations['admin_impersonate_err_csrf'] ?? 'Ошибка безопасности. Обновите страницу.'); - if ($code === 'NO_EMAIL') $msg = ($translations['admin_impersonate_err_no_email'] ?? 'Укажите e-mail практикующего или клиента.'); - if ($code === 'NOT_FOUND') $msg = ($translations['admin_impersonate_err_not_found'] ?? 'Пользователь с таким e-mail не найден.'); - if ($code === 'ROLE_FORBIDDEN') $msg = ($translations['admin_impersonate_err_role_forbidden'] ?? 'Невозможно подключиться под этим типом пользователя.'); + if ($code === 'CSRF') { + $msg = ($translations['admin_impersonate_err_csrf'] ?? 'Ошибка безопасности. Обновите страницу.'); + } - $flash = ['type'=>'danger','msg'=>$msg]; + if ($code === 'NO_EMAIL') { + $msg = ($translations['admin_impersonate_err_no_email'] ?? 'Укажите e-mail практикующего или клиента.'); + } + + if ($code === 'NOT_FOUND') { + $msg = ($translations['admin_impersonate_err_not_found'] ?? 'Пользователь с таким e-mail не найден.'); + } + + if ($code === 'ROLE_FORBIDDEN') { + $msg = ($translations['admin_impersonate_err_role_forbidden'] ?? 'Невозможно подключиться под этим типом пользователя.'); + } + + $flash = ['type' => 'danger', 'msg' => $msg]; + } } -} -// Indicateur si on est déjà en mode impersonation (admin_impersonate présent) -$isImpersonating = !empty($_SESSION['admin_impersonate']) && is_array($_SESSION['admin_impersonate']); + // Indicateur si on est déjà en mode impersonation (admin_impersonate présent) + $isImpersonating = ! empty($_SESSION['admin_impersonate']) && is_array($_SESSION['admin_impersonate']); ?>
-

+

- + - +
-
+
- +
- + + placeholder="">
- +

- +

diff --git a/admin/tools/retour_admin.php b/admin/tools/retour_admin.php index e0a173e..3e51513 100644 --- a/admin/tools/retour_admin.php +++ b/admin/tools/retour_admin.php @@ -1,26 +1,38 @@ li, +.menu-panel .dropdown-item { + font-size: 16px; + line-height: 1.25; + white-space: nowrap; + cursor: pointer; + transition: background 0.2s ease; +} + +/* séparateurs entre blocs du menu principal */ +.menu-panel > li + li, +.menu-panel .dropdown-item + .dropdown-item { + border-top: 1px solid #f1f1f1; +} + +.menu-panel > li:hover, +.menu-panel .dropdown-item:hover { + background: #f8f9fa; +} + +.menu-panel a { + color: inherit; + text-decoration: none; + display: block; + cursor: pointer; +} + +/* ==== MENU CUSTOM ==== */ .menu-wrap { position: relative; - display: inline-block; } + .dropdown-menu-custom { position: absolute; top: calc(100% + 8px); right: 0; - background: #fff; - border: 1px solid #dee2e6; - border-radius: 8px; list-style: none; - padding: 0; margin: 0; - width: 230px; - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08); display: none; z-index: 1100; } -.dropdown-menu-custom li { + +.dropdown-menu-custom.is-open { + display: block; +} + +/* ==== Sous-menu interne ==== */ +.has-submenu { + background: #fff; +} + +.has-submenu .submenu-toggle { + width: 100%; + text-align: left; + background: none; + border: 0; padding: 12px 18px; - border-bottom: 1px solid #f1f1f1; + font-size: 16px; + line-height: 1.25; + font-weight: 400; + display: flex; + align-items: center; + justify-content: space-between; cursor: pointer; - font-size: 15px; + transition: background 0.2s ease; } -.dropdown-menu-custom li:last-child { - border-bottom: none; -} -.dropdown-menu-custom li:hover { + +.has-submenu .submenu-toggle:hover { background: #f8f9fa; } +.submenu-arrow { + font-size: 14px; + opacity: 0.7; + transition: transform 0.2s ease; +} + +.has-submenu.open .submenu-arrow { + transform: rotate(180deg); +} + +.submenu { + list-style: none; + margin: 0; + padding: 0 0 8px 0; + background: #fff; +} + +.submenu > li { + padding: 10px 18px 10px 32px; + font-size: 15px; + line-height: 1.25; + cursor: pointer; + transition: background 0.2s ease; + white-space: nowrap; +} + +.submenu > li:hover { + background: #f8f9fa; +} + +.submenu > li + li { + border-top: 1px solid #f7f7f7; +} + +.submenu[hidden] { + display: none; +} + +/* bouton logout */ +.menu-item-form { + padding: 0; +} + +.menu-logout-form { + margin: 0; +} + +.menu-logout-btn { + width: 100%; + text-align: left; + background: none; + border: 0; + padding: 12px 18px; + font-size: 16px; + line-height: 1.25; + cursor: pointer; +} + +.menu-logout-btn:hover { + background: #f8f9fa; +} + +/* sécurité overflow */ +.site-header, +.header-actions, +.menu-wrap, +.dropdown { + overflow: visible; +} + +/* bouton avatar */ +#btnMenu { + background: none; + border: none; + padding: 0; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + padding-left: 10px; +} + +#btnMenu:focus, +#btnMenu:active { + outline: none; + box-shadow: none; +} + /* ======================== Mobile-first (≤ 956px) ======================== */ @media (max-width: 956px) { /* Garde-fous */ @@ -398,3 +534,34 @@ background: #38378b; /* bleu foncé */ color: #fff; } + +.menu-item-form { + padding: 0 !important; + list-style: none; +} + +.menu-logout-form { + margin: 0; + width: 100%; +} + +.menu-logout-btn { + all: unset; + box-sizing: border-box; + display: block; + width: 100%; + padding: 12px 16px; + cursor: pointer; + font: inherit; + color: inherit; + text-align: left; +} + +.menu-logout-btn:hover { + background: #f3f4f6; +} + +.menu-logout-btn:focus-visible { + outline: 2px solid #94a3b8; + outline-offset: -2px; +} diff --git a/includes/avatar_initiales.php b/includes/avatar_initiales.php index 170fc49..79cacf0 100644 --- a/includes/avatar_initiales.php +++ b/includes/avatar_initiales.php @@ -7,65 +7,87 @@ /* ========================= Helpers ========================= */ -function is_logged_in(): bool { - $role = (string)($_SESSION['role'] ?? ($_SESSION['user_type'] ?? '')); - if (!empty($_SESSION['reference_id']) && $role !== '') return true; - if (!empty($_SESSION['user_id'])) return true; - if (!empty($_SESSION['client_id'])) return true; - if (!empty($_SESSION['praticien_id'])) return true; - return false; +function is_header_logged_in(): bool +{ + $role = (string) ($_SESSION['role'] ?? ($_SESSION['user_type'] ?? '')); + if (! empty($_SESSION['reference_id']) && $role !== '') { + return true; + } + + if (! empty($_SESSION['user_id'])) { + return true; + } + + if (! empty($_SESSION['client_id'])) { + return true; + } + + if (! empty($_SESSION['praticien_id'])) { + return true; + } + + return false; } /** * Inclut /public/includes/header.php sans DOCTYPE/HTML/HEAD/BODY et sans recharger bootstrap/css. */ -function include_header_clean(string $absPath): void { - global $translations, $lang; - ob_start(); - include $absPath; - $buf = ob_get_clean() ?: ''; +function include_header_clean(string $absPath): void +{ + global $translations, $lang; + require_once APP_HTDOCS . '/public/includes/securite.php'; + ob_start(); + include $absPath; // /public/includes/header.php + $buf = ob_get_clean() ?: ''; - $buf = preg_replace('~]*>~i', '', $buf); - $buf = preg_replace('~]*>~i', '', $buf); - $buf = preg_replace('~]*>.*?~is', '', $buf); - $buf = preg_replace('~]*>~i', '', $buf); + // 0) DOCTYPE + … + $buf = preg_replace('~]*>~i', '', $buf); + $buf = preg_replace('~]*>~i', '', $buf); - // éviter d’écraser la home - $buf = preg_replace('~]+href="[^"]*(bootstrap[^"]*\.css|mobil_style\.css|site_style\.css|fonts\.googleapis)[^"]*"[^>]*>~i', '', $buf); - $buf = preg_replace('~]+src="[^"]*bootstrap[^"]*bundle[^"]*\.js[^"]*"[^>]*>\s*~i', '', $buf); + // 1) retirer … et ouverts/fermés + $buf = preg_replace('~]*>.*?~is', '', $buf); + $buf = preg_replace('~]*>~i', '', $buf); - echo $buf; + // 2) retirer les CSS “globaux” pour éviter d’écraser la home + $buf = preg_replace('~]+href="[^"]*(bootstrap[^"]*\.css|mobil_style\.css|site_style\.css|fonts\.googleapis)[^"]*"[^>]*>~i', '', $buf); + + // 3) retirer le ~i', '', $buf); + + echo $buf; // garde
+ tes scripts inline utiles } -function prepare_secure_header_context(): void { - $pdo = $GLOBALS['pdo'] ?? null; - if (!($pdo instanceof PDO)) return; +function prepare_secure_header_context(): void +{ + $pdo = $GLOBALS['pdo'] ?? null; + if (! ($pdo instanceof PDO)) { + return; + } - $role = strtolower((string)($_SESSION['role'] ?? '')); - $refId = (int)($_SESSION['reference_id'] ?? 0); + $role = strtolower((string) ($_SESSION['role'] ?? '')); + $refId = (int) ($_SESSION['reference_id'] ?? 0); - - - // Initiales client/praticien - if ($refId > 0 && ($role === 'client' || $role === 'praticien')) { - try { - $table = ($role === 'client') ? 'client' : 'praticien'; - $st = $pdo->prepare("SELECT nom, prenom FROM {$table} WHERE id = ? LIMIT 1"); - $st->execute([$refId]); - $u = $st->fetch(PDO::FETCH_ASSOC) ?: []; - $nom = trim((string)($u['nom'] ?? '')); - $prenom = trim((string)($u['prenom'] ?? '')); - $ini = mb_strtoupper( - mb_substr($prenom, 0, 1, 'UTF-8') . mb_substr($nom, 0, 1, 'UTF-8'), - 'UTF-8' - ); - $GLOBALS['initiales'] = $ini !== '' ? $ini : 'U'; - } catch (Throwable $e) { - $GLOBALS['initiales'] = 'U'; + // Initiales client/praticien + if ($refId > 0 && ($role === 'client' || $role === 'praticien')) { + try { + $table = ($role === 'client') ? 'client' : 'praticien'; + $st = $pdo->prepare("SELECT nom, prenom FROM {$table} WHERE id = ? LIMIT 1"); + $st->execute([$refId]); + $u = $st->fetch(PDO::FETCH_ASSOC) ?: []; + $nom = trim((string) ($u['nom'] ?? '')); + $prenom = trim((string) ($u['prenom'] ?? '')); + $ini = mb_strtoupper( + mb_substr($prenom, 0, 1, 'UTF-8') . mb_substr($nom, 0, 1, 'UTF-8'), + 'UTF-8' + ); + $GLOBALS['initiales'] = $ini !== '' ? $ini : 'U'; + } catch (Throwable $e) { + $GLOBALS['initiales'] = 'U'; + } } - } } // Si connecté -> préparer le contexte header sécurisé (sans redirection) -$logged = is_logged_in(); -if ($logged) prepare_secure_header_context(); - +$logged = is_header_logged_in(); +if ($logged) { + prepare_secure_header_context(); +} diff --git a/index.php b/index.php index ce3eb11..a13d619 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,17 @@ +