V10.1 stable avec correction securité,bug sur csrf dans index
This commit is contained in:
1 parent
e9d0010451
commit
21f34c64c9
11 files changed
+1136
-445
No files matched your search
@@ -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)
|
||||
<IfModule mod_rewrite.c>
|
||||
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]
|
||||
</IfModule>
|
||||
|
||||
# Block xmlrpc.php
|
||||
<Files xmlrpc.php>
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
</Files>
|
||||
# END Security
|
||||
|
||||
# --- Security headers ---
|
||||
<IfModule mod_headers.c>
|
||||
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'"
|
||||
|
||||
</IfModule>
|
||||
|
||||
# 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
|
||||
<FilesMatch "^(config\.php|config\.sample\.php|composer\.json|composer\.lock|package\.json|yarn\.lock)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
# 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
|
||||
|
||||
<Files "sw.js">
|
||||
@@ -68,7 +296,7 @@ AddType application/manifest+json .webmanifest
|
||||
Header set Expires "0"
|
||||
</Files>
|
||||
|
||||
# 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
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
@@ -81,5 +309,3 @@ SetEnvIf Request_URI "^/visio/join\.php$" IS_VISIO=1
|
||||
</IfModule>
|
||||
|
||||
DirectoryIndex index.php index.html
|
||||
|
||||
|
||||
+128
-88
@@ -1,7 +1,4 @@
|
||||
<?php
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('display_startup_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
require_once __DIR__ . '/../public/includes/init_admin.php';
|
||||
|
||||
// FIX: cast pour comparaison stricte
|
||||
@@ -110,74 +107,110 @@
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu-custom menu-panel" id="menu" style="display:none; min-width:220px;">
|
||||
<li onclick="window.open('https://staging.uslugi.fr/matomo/index.php','_blank')">
|
||||
<?php echo h($translations['admin_analytic'] ?? 'Analytics') ?>
|
||||
</li>
|
||||
<ul class="dropdown-menu-custom menu-panel" id="menu" aria-hidden="true">
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('add_video')) ?>'">
|
||||
<?php echo h($translations['add_video'] ?? 'Ajouter une vidéo') ?>
|
||||
</li>
|
||||
<li class="has-submenu">
|
||||
<button type="button" class="submenu-toggle" aria-expanded="false">
|
||||
<?php echo h($translations['menu_stats'] ?? 'Statistiques & suivi') ?>
|
||||
<span class="submenu-arrow">▾</span>
|
||||
</button>
|
||||
<ul class="submenu" hidden>
|
||||
<li onclick="window.open('https://staging.uslugi.fr/matomo/index.php','_blank')">
|
||||
<?php echo h($translations['admin_analytic'] ?? 'Analytics') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('logs')) ?>'">
|
||||
<?php echo h($translations['afficher_logs'] ?? 'Logs') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('audit')) ?>'">
|
||||
<?php echo h($translations['audit'] ?? 'Audit') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('new_inscrits')) ?>'">
|
||||
<?php echo h($translations['les_inscrits'] ?? 'Les Inscrits') ?>
|
||||
</li>
|
||||
<li class="has-submenu">
|
||||
<button type="button" class="submenu-toggle" aria-expanded="false">
|
||||
<?php echo h($translations['menu_users'] ?? 'Utilisateurs') ?>
|
||||
<span class="submenu-arrow">▾</span>
|
||||
</button>
|
||||
<ul class="submenu" hidden>
|
||||
<li onclick="location.href='<?php echo h(admin_url('new_inscrits')) ?>'">
|
||||
<?php echo h($translations['les_inscrits'] ?? 'Les inscrits') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('gerer_praticiens')) ?>'">
|
||||
<?php echo h($translations['gerer_praticiens'] ?? 'Gérer praticiens') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('connecter_en_admin')) ?>'">
|
||||
<?php echo h($translations['connecter_en_admin'] ?? 'Войти от имени пользователя') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('mail_praticiens')) ?>'">
|
||||
<?php echo h($translations['mail_praticiens'] ?? 'Рассылка практикующим') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('mailing_client')) ?>'">
|
||||
<?php echo h($translations['mail_clients'] ?? 'Рассылка клиентам') ?>
|
||||
</li>
|
||||
<li class="has-submenu">
|
||||
<button type="button" class="submenu-toggle" aria-expanded="false">
|
||||
<?php echo h($translations['menu_communication'] ?? 'Communication') ?>
|
||||
<span class="submenu-arrow">▾</span>
|
||||
</button>
|
||||
<ul class="submenu" hidden>
|
||||
<li onclick="location.href='<?php echo h(admin_url('mail_praticiens')) ?>'">
|
||||
<?php echo h($translations['mail_praticiens'] ?? 'Рассылка практикующим') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('mailing_client')) ?>'">
|
||||
<?php echo h($translations['mail_clients'] ?? 'Рассылка клиентам') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('afficher_messages')) ?>'">
|
||||
<?php echo h($translations['afficher_messages'] ?? 'Afficher Messages') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('connecter_en_admin')) ?>'">
|
||||
<?php echo h($translations['connecter_en_admin'] ?? 'Войти от имени пользователя') ?>
|
||||
</li>
|
||||
<li class="has-submenu">
|
||||
<button type="button" class="submenu-toggle" aria-expanded="false">
|
||||
<?php echo h($translations['menu_content'] ?? 'Contenu') ?>
|
||||
<span class="submenu-arrow">▾</span>
|
||||
</button>
|
||||
<ul class="submenu" hidden>
|
||||
<li onclick="location.href='<?php echo h(admin_url('add_video')) ?>'">
|
||||
<?php echo h($translations['add_video'] ?? 'Ajouter une vidéo') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('gerer_praticiens')) ?>'">
|
||||
<?php echo h($translations['gerer_praticiens'] ?? 'Gérer praticiens') ?>
|
||||
</li>
|
||||
<li class="has-submenu">
|
||||
<button type="button" class="submenu-toggle" aria-expanded="false">
|
||||
<?php echo h($translations['menu_maintenance'] ?? 'Maintenance') ?>
|
||||
<span class="submenu-arrow">▾</span>
|
||||
</button>
|
||||
<ul class="submenu" hidden>
|
||||
<li onclick="location.href='<?php echo h(admin_url('cleanup_inscriptions')) ?>'">
|
||||
<?php echo h($translations['supp_inscr'] ?? 'Supp. inscriptions') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('clean_uploads')) ?>'">
|
||||
<?php echo h($translations['supp_uploads'] ?? 'Supp. uploads') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('purge_conversations')) ?>'">
|
||||
<?php echo h($translations['purger_messages'] ?? 'Supp. messages') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('purger_mess_upload')) ?>'">
|
||||
<?php echo h($translations['purger_mess_upload'] ?? 'Supp. Upload') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('cleanup_rdv')) ?>'">
|
||||
<?php echo h($translations['cleanup_rdv'] ?? 'Cleanup RDV') ?>
|
||||
</li>
|
||||
<li onclick="location.href='<?php echo h(admin_url('backup_db')) ?>'">
|
||||
<?php echo h($translations['backup_db'] ?? 'Backup DB') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('cleanup_inscriptions')) ?>'">
|
||||
<?php echo h($translations['supp_inscr'] ?? 'Supp. inscriptions') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('clean_uploads')) ?>'">
|
||||
<?php echo h($translations['supp_uploads'] ?? 'Supp. uploads') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('purge_conversations')) ?>'">
|
||||
<?php echo h($translations['purger_messages'] ?? 'Supp. messages') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('purger_mess_upload')) ?>'">
|
||||
<?php echo h($translations['purger_mess_upload'] ?? 'Supp. Upload') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('logs')) ?>'">
|
||||
<?php echo h($translations['afficher_logs'] ?? 'Logs') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('audit')) ?>'">
|
||||
<?php echo h($translations['audit'] ?? 'Audit') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('backup_db')) ?>'">
|
||||
<?php echo h($translations['backup_db'] ?? 'Backup DB') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('cleanup_rdv')) ?>'">
|
||||
<?php echo h($translations['cleanup_rdv'] ?? 'Cleanup RDV') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='<?php echo h(admin_url('afficher_messages')) ?>'">
|
||||
<?php echo h($translations['afficher_messages'] ?? 'Afficher Messages') ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='/public/deconnexion.php'" class="text-danger">
|
||||
<?php echo h($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
|
||||
</li>
|
||||
</ul>
|
||||
<li class="menu-item-form">
|
||||
<form method="POST" action="/public/deconnexion.php" class="menu-logout-form">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<button type="submit" class="menu-logout-btn">
|
||||
<?php echo htmlspecialchars($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -197,7 +230,37 @@
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const toggles = document.querySelectorAll('.submenu-toggle');
|
||||
|
||||
toggles.forEach(toggle => {
|
||||
toggle.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
const parent = this.closest('.has-submenu');
|
||||
const submenu = parent.querySelector('.submenu');
|
||||
const isOpen = parent.classList.contains('open');
|
||||
|
||||
// ferme les autres
|
||||
document.querySelectorAll('.has-submenu.open').forEach(item => {
|
||||
if (item !== parent) {
|
||||
item.classList.remove('open');
|
||||
const sub = item.querySelector('.submenu');
|
||||
const btn = item.querySelector('.submenu-toggle');
|
||||
if (sub) sub.hidden = true;
|
||||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
// toggle courant
|
||||
parent.classList.toggle('open', !isOpen);
|
||||
submenu.hidden = isOpen;
|
||||
this.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function toggleMenu() {
|
||||
const menu = document.getElementById('menu');
|
||||
@@ -226,29 +289,6 @@
|
||||
if (e.key === 'Escape') closeMenu();
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.submenu-toggle');
|
||||
if (!btn) return;
|
||||
|
||||
const submenuId = btn.getAttribute('aria-controls');
|
||||
const submenu = document.getElementById(submenuId);
|
||||
if (!submenu) return;
|
||||
|
||||
const isOpen = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', String(!isOpen));
|
||||
submenu.hidden = isOpen;
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
function closeSubmenus() {
|
||||
document
|
||||
.querySelectorAll('.submenu-toggle[aria-expanded="true"]')
|
||||
.forEach(btn => {
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
const sub = document.getElementById(btn.getAttribute('aria-controls'));
|
||||
if (sub) sub.hidden = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
<?php
|
||||
// admin/tools/connecter_en_admin.php
|
||||
require_once __DIR__ . '/../../public/includes/init_admin.php';
|
||||
// admin/tools/connecter_en_admin.php
|
||||
require_once __DIR__ . '/../../public/includes/init_admin.php';
|
||||
|
||||
const ADMIN_PRACTICIEN_ID = 88888888;
|
||||
$me = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($me !== ADMIN_PRACTICIEN_ID) {
|
||||
const ADMIN_PRACTICIEN_ID = 88888888;
|
||||
$me = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($me !== ADMIN_PRACTICIEN_ID) {
|
||||
http_response_code(403);
|
||||
echo '<div class="container py-5"><div class="alert alert-danger">'.htmlspecialchars(($translations['access_denied'] ?? 'Доступ запрещён.'), ENT_QUOTES, 'UTF-8').'</div></div>';
|
||||
echo '<div class="container py-5"><div class="alert alert-danger">' . htmlspecialchars(($translations['access_denied'] ?? 'Доступ запрещён.'), ENT_QUOTES, 'UTF-8') . '</div></div>';
|
||||
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']);
|
||||
|
||||
?>
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-3"><?= e($translations['admin_impersonate_title'] ?? 'Подключиться как пользователь') ?></h2>
|
||||
<h2 class="mb-3"><?php echo e($translations['admin_impersonate_title'] ?? 'Подключиться как пользователь') ?></h2>
|
||||
|
||||
<?php if ($isImpersonating): ?>
|
||||
<div class="alert alert-warning">
|
||||
<?= e($translations['admin_impersonate_warn_active'] ?? 'Внимание: сейчас вы авторизованы как другой пользователь.') ?>
|
||||
<?php echo e($translations['admin_impersonate_warn_active'] ?? 'Внимание: сейчас вы авторизованы как другой пользователь.') ?>
|
||||
<a href="/admin/index.php?page=retour_admin" class="btn btn-sm btn-outline-danger ms-2" style="border-radius:999px;">
|
||||
<?= e($translations['admin_impersonate_btn_back_admin'] ?? 'Вернуться в режим администратора') ?>
|
||||
<?php echo e($translations['admin_impersonate_btn_back_admin'] ?? 'Вернуться в режим администратора') ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($flash): ?>
|
||||
<div class="alert alert-<?= e($flash['type'] ?? 'info') ?>"><?= e($flash['msg'] ?? '') ?></div>
|
||||
<div class="alert alert-<?php echo e($flash['type'] ?? 'info') ?>"><?php echo e($flash['msg'] ?? '') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" class="mb-4">
|
||||
<input type="hidden" name="csrf" value="<?= e($CSRF) ?>">
|
||||
<input type="hidden" name="csrf" value="<?php echo e($CSRF) ?>">
|
||||
<input type="hidden" name="do" value="impersonate">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= e($translations['admin_impersonate_label_email'] ?? 'E-mail практикующего или клиента') ?></label>
|
||||
<label class="form-label"><?php echo e($translations['admin_impersonate_label_email'] ?? 'E-mail практикующего или клиента') ?></label>
|
||||
<input type="email" name="email" class="form-control" required
|
||||
placeholder="<?= e($translations['admin_impersonate_ph_email'] ?? 'example@domaine.fr') ?>">
|
||||
placeholder="<?php echo e($translations['admin_impersonate_ph_email'] ?? 'example@domaine.fr') ?>">
|
||||
<div class="form-text">
|
||||
<?= e($translations['admin_impersonate_help'] ?? 'Администратор будет авторизован под этим пользователем без ввода пароля.') ?>
|
||||
<?php echo e($translations['admin_impersonate_help'] ?? 'Администратор будет авторизован под этим пользователем без ввода пароля.') ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-danger" style="border-radius:999px;">
|
||||
<?= e($translations['admin_impersonate_btn_submit'] ?? 'Подключиться под этим пользователем') ?>
|
||||
<?php echo e($translations['admin_impersonate_btn_submit'] ?? 'Подключиться под этим пользователем') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="text-muted mb-0">
|
||||
<?= e($translations['admin_impersonate_footer'] ?? 'Все действия должны соответствовать политике конфиденциальности. Доступ к этой функции есть только у супер-администратора.') ?>
|
||||
<?php echo e($translations['admin_impersonate_footer'] ?? 'Все действия должны соответствовать политике конфиденциальности. Доступ к этой функции есть только у супер-администратора.') ?>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,26 +1,38 @@
|
||||
<?php
|
||||
// admin/tools/retour_admin.php
|
||||
declare(strict_types=1);
|
||||
declare (strict_types = 1);
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
@session_start();
|
||||
}
|
||||
|
||||
// On ne met pas require_login_or_redirect ici, car on peut être sous un compte client/praticien
|
||||
// On se contente de vérifier qu'on a bien une info d’admin d’origine.
|
||||
if (!empty($_SESSION['admin_impersonate']) && is_array($_SESSION['admin_impersonate'])) {
|
||||
// On ne met pas require_login_or_redirect ici,
|
||||
// car on peut être sous un compte client/praticien.
|
||||
// On vérifie seulement qu'on a bien l'admin d'origine.
|
||||
if (! empty($_SESSION['admin_impersonate']) && is_array($_SESSION['admin_impersonate'])) {
|
||||
$orig = $_SESSION['admin_impersonate'];
|
||||
|
||||
if (isset($orig['admin_user_id'])) {
|
||||
$_SESSION['user_id'] = $orig['admin_user_id'];
|
||||
session_regenerate_id(true);
|
||||
|
||||
unset(
|
||||
$_SESSION['client_id'],
|
||||
$_SESSION['praticien_id'],
|
||||
$_SESSION['secretaire_id'],
|
||||
$_SESSION['admin_id'],
|
||||
);
|
||||
|
||||
if (isset($orig['admin_compte_id'])) {
|
||||
$_SESSION['compte_id'] = (int) $orig['admin_compte_id'];
|
||||
}
|
||||
if (isset($orig['admin_reference_id'])) {
|
||||
$_SESSION['reference_id'] = $orig['admin_reference_id'];
|
||||
$_SESSION['reference_id'] = (int) $orig['admin_reference_id'];
|
||||
}
|
||||
if (isset($orig['admin_role'])) {
|
||||
$_SESSION['role'] = $orig['admin_role'];
|
||||
$_SESSION['role'] = (string) $orig['admin_role'];
|
||||
}
|
||||
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
unset($_SESSION['admin_impersonate']);
|
||||
}
|
||||
|
||||
|
||||
+181
-14
@@ -7,38 +7,174 @@
|
||||
:root {
|
||||
--header: #f1f1f1;
|
||||
}
|
||||
/* ==== VISUEL COMMUN POUR TOUS LES MENUS ==== */
|
||||
.menu-panel {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
||||
min-width: 230px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Items menu principal */
|
||||
.menu-panel > 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;
|
||||
}
|
||||
@@ -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('~<!doctype\s+html[^>]*>~i', '', $buf);
|
||||
$buf = preg_replace('~</?html\b[^>]*>~i', '', $buf);
|
||||
$buf = preg_replace('~<head\b[^>]*>.*?</head>~is', '', $buf);
|
||||
$buf = preg_replace('~</?body\b[^>]*>~i', '', $buf);
|
||||
// 0) DOCTYPE + <html>…</html>
|
||||
$buf = preg_replace('~<!doctype\s+html[^>]*>~i', '', $buf);
|
||||
$buf = preg_replace('~</?html\b[^>]*>~i', '', $buf);
|
||||
|
||||
// éviter d’écraser la home
|
||||
$buf = preg_replace('~<link[^>]+href="[^"]*(bootstrap[^"]*\.css|mobil_style\.css|site_style\.css|fonts\.googleapis)[^"]*"[^>]*>~i', '', $buf);
|
||||
$buf = preg_replace('~<script[^>]+src="[^"]*bootstrap[^"]*bundle[^"]*\.js[^"]*"[^>]*>\s*</script>~i', '', $buf);
|
||||
// 1) retirer <head>…</head> et <body> ouverts/fermés
|
||||
$buf = preg_replace('~<head\b[^>]*>.*?</head>~is', '', $buf);
|
||||
$buf = preg_replace('~</?body\b[^>]*>~i', '', $buf);
|
||||
|
||||
echo $buf;
|
||||
// 2) retirer les <link> CSS “globaux” pour éviter d’écraser la home
|
||||
$buf = preg_replace('~<link[^>]+href="[^"]*(bootstrap[^"]*\.css|mobil_style\.css|site_style\.css|fonts\.googleapis)[^"]*"[^>]*>~i', '', $buf);
|
||||
|
||||
// 3) retirer le <script> Bootstrap CDN (on le charge déjà en bas de la page)
|
||||
$buf = preg_replace('~<script[^>]+src="[^"]*bootstrap[^"]*bundle[^"]*\.js[^"]*"[^>]*>\s*</script>~i', '', $buf);
|
||||
|
||||
echo $buf; // garde <header> + 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();
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('error_log', __DIR__ . '/php-debug.log');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
register_shutdown_function(function () {
|
||||
$e = error_get_last();
|
||||
file_put_contents(__DIR__ . '/php-debug.log',
|
||||
"SHUTDOWN: " . print_r($e, true) . "\n",
|
||||
FILE_APPEND
|
||||
);
|
||||
});
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
||||
require_once __DIR__ . '/includes/search_helpers.php';
|
||||
@@ -172,8 +183,9 @@
|
||||
|
||||
<body>
|
||||
<?php if (! empty($logged)):
|
||||
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
|
||||
include_header_clean(__DIR__ . '/public/includes/header.php');
|
||||
else: ?>
|
||||
|
||||
<!-- Header public identique -->
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
@@ -294,9 +306,9 @@ else: ?>
|
||||
FROM praticien
|
||||
WHERE photo IS NOT NULL
|
||||
AND photo <> ''
|
||||
AND date_expiration > DATE_ADD(NOW(), INTERVAL 2 DAY)
|
||||
AND date_expiration >= NOW()
|
||||
ORDER BY id DESC
|
||||
LIMIT 50
|
||||
LIMIT 100
|
||||
")->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (! $ids) {
|
||||
@@ -315,12 +327,20 @@ else: ?>
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, nom, prenom, specialite, photo
|
||||
FROM praticien
|
||||
WHERE id IN ($placeholders)
|
||||
SELECT
|
||||
p.id,
|
||||
COALESCE(NULLIF(pt.nom_affiche, ''), p.nom) AS nom,
|
||||
COALESCE(NULLIF(pt.prenom_affiche, ''), p.prenom) AS prenom,
|
||||
COALESCE(NULLIF(pt.specialite, ''), p.specialite) AS specialite,
|
||||
p.photo
|
||||
FROM praticien p
|
||||
LEFT JOIN praticien_traduction pt
|
||||
ON pt.praticien_id = p.id
|
||||
AND pt.langue = ?
|
||||
WHERE p.id IN ($placeholders)
|
||||
");
|
||||
$stmt->execute($ids);
|
||||
|
||||
$stmt->execute(array_merge([$lang], $ids));
|
||||
$randomPros = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $randomPros) {
|
||||
@@ -378,6 +398,7 @@ else: ?>
|
||||
$note = (float) $stats['m'];
|
||||
$count = (int) $stats['c'];
|
||||
$w = percent_from_5($note);
|
||||
|
||||
?>
|
||||
<article class="hp-card">
|
||||
|
||||
@@ -420,7 +441,8 @@ else: ?>
|
||||
<span><?php echo h($availability) ?></span>
|
||||
</div>
|
||||
|
||||
<a class="hp-btn" href="<?php echo h($detailUrl) ?>">
|
||||
<a class="hp-btn"
|
||||
href="/public/client/reserver_rdv.php?praticien_id=<?php echo $pid ?>">
|
||||
<?php echo h($translations['home_btn_book'] ?? '') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
+352
-194
@@ -1,189 +1,292 @@
|
||||
<?php
|
||||
// /public/contact_admin.php (SAFE to include in header)
|
||||
// - Si appelé directement par URL : répond aux GET ?meta=1 et POST (JSON) et exit.
|
||||
// - Si inclus (ex. dans header.php) : n'injecte le JS que pour des pages HTML GET,
|
||||
// ne touche jamais au Content-Type ni ne fait exit.
|
||||
// /public/contact_admin.php (SAFE to include in header)
|
||||
// - Si appelé directement par URL : répond aux GET ?meta=1 et POST (JSON) et exit.
|
||||
// - Si inclus (ex. dans header.php) : n'injecte le JS que pour des pages HTML GET,
|
||||
// ne touche jamais au Content-Type ni ne fait exit.
|
||||
|
||||
declare(strict_types=1);
|
||||
$IS_DIRECT = isset($_SERVER['SCRIPT_FILENAME'])
|
||||
&& realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME']);
|
||||
require_once __DIR__ . '/../../includes/bootstrap_global.php';
|
||||
declare (strict_types = 1);
|
||||
|
||||
/* ======================= Helpers ======================= */
|
||||
$h = static fn($s)=>htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
|
||||
$ipToBinary = static function (?string $ip): ?string {
|
||||
if (!$ip) return null;
|
||||
$bin = @inet_pton($ip);
|
||||
return $bin === false ? null : $bin;
|
||||
};
|
||||
$nz = static function($a, $b) {
|
||||
$a = trim((string)($a ?? ''));
|
||||
$b = trim((string)($b ?? ''));
|
||||
return $a !== '' ? $a : $b;
|
||||
};
|
||||
$IS_DIRECT = isset($_SERVER['SCRIPT_FILENAME'])
|
||||
&& realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME']);
|
||||
|
||||
/* ======================= CSRF ======================= */
|
||||
if (empty($_SESSION['pub_csrf'])) $_SESSION['pub_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['pub_csrf'];
|
||||
require_once __DIR__ . '/../../includes/bootstrap_global.php';
|
||||
global $pdo;
|
||||
|
||||
/* ======================= Auth / Roles ======================= */
|
||||
$ALLOWED_ROLES = ['client','praticien','admin', 'secretaire', 'admin-praticien'];
|
||||
$isLogged = !empty($_SESSION['compte_id']);
|
||||
$user = ['compte_id'=>null,'role'=>null,'ref_id'=>null,'email'=>null];
|
||||
|
||||
if ($isLogged) {
|
||||
try {
|
||||
$st = $pdo->prepare("SELECT id, email, role, reference_id FROM compte WHERE id=? LIMIT 1");
|
||||
$st->execute([(int)$_SESSION['compte_id']]);
|
||||
if ($row = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
$user = [
|
||||
'compte_id' => (int)$row['id'],
|
||||
'role' => (string)$row['role'],
|
||||
'ref_id' => (int)$row['reference_id'],
|
||||
'email' => (string)$row['email'],
|
||||
];
|
||||
if (! isset($pdo) || ! ($pdo instanceof PDO)) {
|
||||
return;
|
||||
}
|
||||
} catch(Throwable $e) {}
|
||||
}
|
||||
$roleAllowed = $isLogged && in_array((string)$user['role'], $ALLOWED_ROLES, true);
|
||||
/* ======================= Helpers ======================= */
|
||||
$h = static fn($s) => htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
/* ======================= DB Utils ======================= */
|
||||
$tableExists = static function(PDO $pdo, string $t): bool {
|
||||
$st = $pdo->prepare("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?");
|
||||
$st->execute([$t]);
|
||||
return (bool)$st->fetchColumn();
|
||||
};
|
||||
$ipToBinary = static function (?string $ip): ?string {
|
||||
if (! $ip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ======================= DIRECT: POST handler ======================= */
|
||||
if ($IS_DIRECT && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$out = ['ok'=>false,'msg'=>''];
|
||||
$bin = @inet_pton($ip);
|
||||
return $bin === false ?null : $bin;
|
||||
};
|
||||
|
||||
try {
|
||||
if (!$roleAllowed) throw new Exception('FORBIDDEN');
|
||||
$nz = static function ($a, $b) {
|
||||
$a = trim((string) ($a ?? ''));
|
||||
$b = trim((string) ($b ?? ''));
|
||||
return $a !== '' ? $a : $b;
|
||||
};
|
||||
|
||||
// anti-spam 10s
|
||||
$last = (int)($_SESSION['last_contact_at'] ?? 0);
|
||||
if (time() - $last < 10) throw new Exception('TOO_MANY');
|
||||
/* ======================= CSRF ======================= */
|
||||
if (empty($_SESSION['pub_csrf'])) {
|
||||
$_SESSION['pub_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
$CSRF = $_SESSION['pub_csrf'];
|
||||
|
||||
// CSRF
|
||||
if (!hash_equals($CSRF, $_POST['csrf'] ?? '')) throw new Exception('CSRF');
|
||||
/* ======================= Auth / Roles ======================= */
|
||||
$ALLOWED_ROLES = ['client', 'praticien', 'admin', 'secretaire', 'admin-praticien'];
|
||||
|
||||
// Fallback intelligent: POST sinon PREF (mais pas vide)
|
||||
$nom = $nz($_POST['nom'] ?? null, $_SESSION['_caf_nom'] ?? '');
|
||||
$prenom = $nz($_POST['prenom'] ?? null, $_SESSION['_caf_prenom'] ?? '');
|
||||
$email = $nz($_POST['email'] ?? null, $_SESSION['_caf_email'] ?? '');
|
||||
$isLogged = ! empty($_SESSION['compte_id']);
|
||||
$user = [
|
||||
'compte_id' => null,
|
||||
'role' => null,
|
||||
'ref_id' => null,
|
||||
'email' => null,
|
||||
];
|
||||
|
||||
$sujet = trim((string)($_POST['sujet'] ?? ''));
|
||||
$msg = trim((string)($_POST['message'] ?? ''));
|
||||
if ($isLogged) {
|
||||
try {
|
||||
$st = $pdo->prepare("SELECT id, email, role, reference_id FROM compte WHERE id = ? LIMIT 1");
|
||||
$st->execute([(int) $_SESSION['compte_id']]);
|
||||
|
||||
if ($nom==='' || $prenom==='' || $sujet==='' || $msg==='') throw new Exception('REQUIRED');
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) throw new Exception('EMAIL');
|
||||
if ($row = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
$user = [
|
||||
'compte_id' => (int) $row['id'],
|
||||
'role' => (string) $row['role'],
|
||||
'ref_id' => (int) $row['reference_id'],
|
||||
'email' => (string) $row['email'],
|
||||
];
|
||||
}
|
||||
} catch (Throwable $e) {}
|
||||
}
|
||||
|
||||
if (!$tableExists($pdo, 'contact_messages')) throw new Exception('TABLE_MISSING');
|
||||
$roleAllowed = $isLogged && in_array((string) $user['role'], $ALLOWED_ROLES, true);
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255);
|
||||
/* ======================= DB Utils ======================= */
|
||||
$tableExists = static function (PDO $pdo, string $t): bool {
|
||||
$st = $pdo->prepare("
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([$t]);
|
||||
return (bool) $st->fetchColumn();
|
||||
};
|
||||
|
||||
$st = $pdo->prepare(
|
||||
"INSERT INTO contact_messages
|
||||
(user_type,user_id,nom,prenom,email,sujet,message,statut,locale,ip,user_agent,created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,NOW())"
|
||||
);
|
||||
$userType = in_array($user['role'], ['client','praticien'], true) ? $user['role'] : 'invite';
|
||||
$st->execute([
|
||||
$userType, $user['ref_id'] ?: null, $nom, $prenom, $email, $sujet, $msg,
|
||||
'nouveau', 'ru', $ipToBinary($ip), $ua
|
||||
]);
|
||||
/* ======================= DIRECT: POST handler ======================= */
|
||||
if ($IS_DIRECT && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$_SESSION['last_contact_at'] = time();
|
||||
$out['ok'] = true;
|
||||
$out['msg'] = $translations['contact_admin_sent_success'];
|
||||
} catch (Throwable $ex) {
|
||||
$code = $ex->getMessage();
|
||||
if ($code==='FORBIDDEN') $out['msg'] = $translations['contact_admin_forbidden'];
|
||||
elseif ($code==='TOO_MANY') $out['msg'] = $translations['contact_admin_too_many'];
|
||||
elseif ($code==='CSRF') $out['msg'] = $translations['contact_admin_csrf'];
|
||||
elseif ($code==='REQUIRED') $out['msg'] = $translations['contact_admin_required_all'];
|
||||
elseif ($code==='EMAIL') $out['msg'] = $translations['contact_admin_invalid_email'];
|
||||
elseif ($code==='TABLE_MISSING') $out['msg'] = $translations['contact_admin_table_missing'];
|
||||
else $out['msg'] = $translations['contact_admin_server_error'];
|
||||
}
|
||||
$out = ['ok' => false, 'msg' => ''];
|
||||
|
||||
echo json_encode($out, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
try {
|
||||
if (! $roleAllowed) {
|
||||
throw new Exception('FORBIDDEN');
|
||||
}
|
||||
|
||||
/* ======================= DIRECT: GET ?meta=1 ======================= */
|
||||
if ($IS_DIRECT && isset($_GET['meta']) && $_GET['meta'] == '1') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
// Empêcher tout cache (SW, proxy, navigateur)
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
header('Vary: Cookie');
|
||||
// Anti-spam simple : 10 secondes
|
||||
$last = (int) ($_SESSION['last_contact_at'] ?? 0);
|
||||
if (time() - $last < 10) {
|
||||
throw new Exception('TOO_MANY');
|
||||
}
|
||||
|
||||
if (!$roleAllowed) {
|
||||
echo json_encode(['ok'=>false,'msg'=>$translations['contact_admin_forbidden']], JSON_UNESCAPED_UNICODE);
|
||||
// CSRF
|
||||
if (! hash_equals($CSRF, $_POST['csrf'] ?? '')) {
|
||||
throw new Exception('CSRF');
|
||||
}
|
||||
|
||||
// Fallback POST sinon session
|
||||
$nom = $nz($_POST['nom'] ?? null, $_SESSION['_caf_nom'] ?? '');
|
||||
$prenom = $nz($_POST['prenom'] ?? null, $_SESSION['_caf_prenom'] ?? '');
|
||||
$email = $nz($_POST['email'] ?? null, $_SESSION['_caf_email'] ?? '');
|
||||
|
||||
$sujet = trim((string) ($_POST['sujet'] ?? ''));
|
||||
$msg = trim((string) ($_POST['message'] ?? ''));
|
||||
|
||||
if ($nom === '' || $prenom === '' || $sujet === '' || $msg === '') {
|
||||
throw new Exception('REQUIRED');
|
||||
}
|
||||
|
||||
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new Exception('EMAIL');
|
||||
}
|
||||
|
||||
// Limites de taille côté serveur
|
||||
if (
|
||||
mb_strlen($nom) > 100 ||
|
||||
mb_strlen($prenom) > 100 ||
|
||||
mb_strlen($email) > 190 ||
|
||||
mb_strlen($sujet) > 180 ||
|
||||
mb_strlen($msg) > 3000
|
||||
) {
|
||||
throw new Exception('TOO_LONG');
|
||||
}
|
||||
|
||||
if (! $tableExists($pdo, 'contact_messages')) {
|
||||
throw new Exception('TABLE_MISSING');
|
||||
}
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255);
|
||||
|
||||
$userType = in_array($user['role'], ['client', 'praticien'], true)
|
||||
? $user['role']
|
||||
: 'invite';
|
||||
|
||||
$st = $pdo->prepare("
|
||||
INSERT INTO contact_messages
|
||||
(user_type, user_id, nom, prenom, email, sujet, message, statut, locale, ip, user_agent, created_at)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$st->execute([
|
||||
$userType,
|
||||
$user['ref_id'] ?: null,
|
||||
$nom,
|
||||
$prenom,
|
||||
$email,
|
||||
$sujet,
|
||||
$msg,
|
||||
'nouveau',
|
||||
'ru',
|
||||
$ipToBinary($ip),
|
||||
$ua,
|
||||
]);
|
||||
|
||||
$_SESSION['last_contact_at'] = time();
|
||||
|
||||
$out['ok'] = true;
|
||||
$out['msg'] = $translations['contact_admin_sent_success'] ?? 'Message envoyé.';
|
||||
} catch (Throwable $ex) {
|
||||
$code = $ex->getMessage();
|
||||
|
||||
if ($code === 'FORBIDDEN') {
|
||||
$out['msg'] = $translations['contact_admin_forbidden'] ?? 'Accès interdit.';
|
||||
} elseif ($code === 'TOO_MANY') {
|
||||
$out['msg'] = $translations['contact_admin_too_many'] ?? 'Merci de patienter avant de renvoyer un message.';
|
||||
} elseif ($code === 'CSRF') {
|
||||
$out['msg'] = $translations['contact_admin_csrf'] ?? 'Session expirée. Rechargez la page.';
|
||||
} elseif ($code === 'REQUIRED') {
|
||||
$out['msg'] = $translations['contact_admin_required_all'] ?? 'Tous les champs sont obligatoires.';
|
||||
} elseif ($code === 'TOO_LONG') {
|
||||
$out['msg'] = $translations['contact_admin_too_long'] ?? 'Le message est trop long.';
|
||||
} elseif ($code === 'EMAIL') {
|
||||
$out['msg'] = $translations['contact_admin_invalid_email'] ?? 'Adresse email invalide.';
|
||||
} elseif ($code === 'TABLE_MISSING') {
|
||||
$out['msg'] = $translations['contact_admin_table_missing'] ?? 'Table manquante.';
|
||||
} else {
|
||||
$out['msg'] = $translations['contact_admin_server_error'] ?? 'Erreur serveur.';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
echo json_encode($out, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ======================= DIRECT: GET ?meta=1 ======================= */
|
||||
if ($IS_DIRECT && isset($_GET['meta']) && $_GET['meta'] == '1') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
header('Vary: Cookie');
|
||||
|
||||
if (! $roleAllowed) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'msg' => $translations['contact_admin_forbidden'] ?? 'Accès interdit.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Token frais à chaque ouverture
|
||||
$_SESSION['pub_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['pub_csrf'];
|
||||
|
||||
$pref = ['nom' => '', 'prenom' => '', 'email' => ''];
|
||||
|
||||
try {
|
||||
if ($user['role'] === 'client') {
|
||||
$st = $pdo->prepare("SELECT prenom, nom, email FROM client WHERE id = ? LIMIT 1");
|
||||
$st->execute([$user['ref_id']]);
|
||||
|
||||
if ($r = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
$pref['prenom'] = $r['prenom'] ?? '';
|
||||
$pref['nom'] = $r['nom'] ?? '';
|
||||
}
|
||||
|
||||
$pref['email'] = $user['email'] ?? '';
|
||||
} elseif (strpos((string) $user['role'], 'praticien') !== false) {
|
||||
$st = $pdo->prepare("SELECT prenom, nom, email FROM praticien WHERE id = ? LIMIT 1");
|
||||
$st->execute([$user['ref_id']]);
|
||||
|
||||
if ($r = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
$pref['prenom'] = $r['prenom'] ?? '';
|
||||
$pref['nom'] = $r['nom'] ?? '';
|
||||
}
|
||||
|
||||
$pref['email'] = $user['email'] ?? '';
|
||||
} else {
|
||||
$pref['email'] = $user['email'] ?? '';
|
||||
}
|
||||
} catch (Throwable $e) {}
|
||||
|
||||
$_SESSION['_caf_nom'] = $pref['nom'];
|
||||
$_SESSION['_caf_prenom'] = $pref['prenom'];
|
||||
$_SESSION['_caf_email'] = $pref['email'];
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'title' => $translations['contact_admin_title'] ?? 'Contacter l’administration',
|
||||
'labels' => [
|
||||
'firstname' => $translations['contact_admin_firstname'] ?? 'Prénom',
|
||||
'lastname' => $translations['contact_admin_lastname'] ?? 'Nom',
|
||||
'email' => $translations['contact_admin_email'] ?? 'Email',
|
||||
'subject' => $translations['contact_admin_subject'] ?? 'Sujet',
|
||||
'message' => $translations['contact_admin_message'] ?? 'Message',
|
||||
'send' => $translations['contact_admin_send'] ?? 'Envoyer',
|
||||
'required' => $translations['contact_admin_required_mandatory'] ?? 'Tous les champs sont obligatoires.',
|
||||
'invalid' => $translations['contact_admin_invalid_email'] ?? 'Email invalide.',
|
||||
],
|
||||
'prefill' => $pref,
|
||||
'csrf' => $CSRF,
|
||||
'endpoint' => '/public/contact_admin.php',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ======================= INCLUDED: inject widget only on HTML GET ======================= */
|
||||
$accept = (string) ($_SERVER['HTTP_ACCEPT'] ?? '');
|
||||
$isHtmlLike = stripos($accept, 'text/html') !== false || empty($accept);
|
||||
$isGet = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET';
|
||||
|
||||
if (! $IS_DIRECT && $isGet && $isHtmlLike) {
|
||||
?>
|
||||
<script>
|
||||
// Échappement HTML côté JS pour éviter XSS dans innerHTML
|
||||
function cafEsc(s) {
|
||||
return String(s ?? '').replace(/[&<>"']/g, function(m) {
|
||||
return {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[m];
|
||||
});
|
||||
}
|
||||
|
||||
// Token **frais** à chaque ouverture
|
||||
$_SESSION['pub_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['pub_csrf'];
|
||||
|
||||
// Préremplissage léger stocké en session pour fallback POST
|
||||
$pref = ['nom'=>'','prenom'=>'','email'=>''];
|
||||
try {
|
||||
if ($user['role'] === 'client') {
|
||||
$st = $pdo->prepare("SELECT prenom, nom, email FROM client WHERE id=? LIMIT 1");
|
||||
$st->execute([$user['ref_id']]);
|
||||
if ($r = $st->fetch(PDO::FETCH_ASSOC)) { $pref['prenom']=$r['prenom']??''; $pref['nom']=$r['nom']??''; }
|
||||
$pref['email'] = $user['email'] ?? '';
|
||||
} elseif (strpos($user['role'], 'praticien') !== false) {
|
||||
$st = $pdo->prepare("SELECT prenom, nom, email FROM praticien WHERE id=? LIMIT 1");
|
||||
$st->execute([$user['ref_id']]);
|
||||
if ($r = $st->fetch(PDO::FETCH_ASSOC)) { $pref['prenom']=$r['prenom']??''; $pref['nom']=$r['nom']??''; }
|
||||
$pref['email'] = $user['email'] ?? '';
|
||||
} else {
|
||||
$pref['email'] = $user['email'] ?? '';
|
||||
}
|
||||
} catch(Throwable $e) {}
|
||||
|
||||
$_SESSION['_caf_nom'] = $pref['nom'];
|
||||
$_SESSION['_caf_prenom'] = $pref['prenom'];
|
||||
$_SESSION['_caf_email'] = $pref['email'];
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'title' => $translations['contact_admin_title'],
|
||||
'labels' => [
|
||||
'firstname' => $translations['contact_admin_firstname'],
|
||||
'lastname' => $translations['contact_admin_lastname'],
|
||||
'email' => $translations['contact_admin_email'],
|
||||
'subject' => $translations['contact_admin_subject'],
|
||||
'message' => $translations['contact_admin_message'],
|
||||
'send' => $translations['contact_admin_send'],
|
||||
'required' => $translations['contact_admin_required_mandatory'],
|
||||
'invalid' => $translations['contact_admin_invalid_email']
|
||||
],
|
||||
'prefill' => $pref,
|
||||
'csrf' => $CSRF,
|
||||
'endpoint' => '/public/contact_admin.php'
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
/* ======================= INCLUDED: inject widget only on HTML GET ======================= */
|
||||
$accept = (string)($_SERVER['HTTP_ACCEPT'] ?? '');
|
||||
$isHtmlLike = stripos($accept, 'text/html') !== false || empty($accept);
|
||||
$isGet = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET';
|
||||
|
||||
if (!$IS_DIRECT && $isGet && $isHtmlLike) {
|
||||
?>
|
||||
<script>
|
||||
// charge SweetAlert2 si absent
|
||||
// Charge SweetAlert2 si absent
|
||||
(function(){
|
||||
if (!window.Swal) {
|
||||
var s = document.createElement('script');
|
||||
@@ -193,9 +296,9 @@ if (!$IS_DIRECT && $isGet && $isHtmlLike) {
|
||||
}
|
||||
})();
|
||||
|
||||
// inject style une seule fois
|
||||
// Inject style une seule fois
|
||||
(function(){
|
||||
if(!document.getElementById('caf-style')){
|
||||
if (!document.getElementById('caf-style')) {
|
||||
const css = `
|
||||
.swal2-popup{border-radius:18px;padding:0}
|
||||
.swal2-title{font-size:1.15rem;font-weight:600;margin:0;padding:18px 22px;color:#111;border-bottom:1px solid #eee}
|
||||
@@ -213,54 +316,88 @@ if (!$IS_DIRECT && $isGet && $isHtmlLike) {
|
||||
.btn-secondary{
|
||||
background:#f3f4f6;color:#111;border:none;border-radius:999px;padding:10px 14px;cursor:pointer
|
||||
}`;
|
||||
const s=document.createElement('style'); s.id='caf-style'; s.textContent=css; document.head.appendChild(s);
|
||||
|
||||
const s = document.createElement('style');
|
||||
s.id = 'caf-style';
|
||||
s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
// bouton #open-contact optionnel : si présent on ouvre, sinon tu peux appeler window.openContactAdmin() toi-même
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('open-contact');
|
||||
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function(e){
|
||||
e.preventDefault(); window.openContactAdmin && window.openContactAdmin();
|
||||
e.preventDefault();
|
||||
|
||||
if (window.openContactAdmin) {
|
||||
window.openContactAdmin();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.openContactAdmin = async function(){
|
||||
try{
|
||||
const res = await fetch('/public/contact_admin.php?meta=1', { credentials: 'same-origin', cache: 'no-store' });
|
||||
try {
|
||||
const res = await fetch('/public/contact_admin.php?meta=1', {
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('HTTP ' + res.status);
|
||||
}
|
||||
|
||||
const meta = await res.json();
|
||||
if (!meta.ok) { return Swal.fire({icon:'error', text: meta.msg || <?= json_encode($translations['contact_admin_js_error'], JSON_UNESCAPED_UNICODE) ?>}); }
|
||||
const L = meta.labels;
|
||||
const hasIdentity = (meta.prefill?.prenom||'').trim() && (meta.prefill?.nom||'').trim() && (meta.prefill?.email||'').trim();
|
||||
|
||||
if (!meta.ok) {
|
||||
return Swal.fire({
|
||||
icon: 'error',
|
||||
text: meta.msg || <?php echo json_encode($translations['contact_admin_js_error'] ?? 'Erreur.', JSON_UNESCAPED_UNICODE) ?>
|
||||
});
|
||||
}
|
||||
|
||||
const L = meta.labels || {};
|
||||
|
||||
const prenomPref = meta.prefill?.prenom || '';
|
||||
const nomPref = meta.prefill?.nom || '';
|
||||
const emailPref = meta.prefill?.email || '';
|
||||
|
||||
const hasIdentity = prenomPref.trim() && nomPref.trim() && emailPref.trim();
|
||||
|
||||
const identityBlock = hasIdentity
|
||||
? `<div class="caf-grid" style="margin-bottom:8px">
|
||||
<div class="caf-row">
|
||||
<input id="caf-pre" class="caf-input" value="${meta.prefill.prenom||''}" readonly>
|
||||
<input id="caf-nom" class="caf-input" value="${meta.prefill.nom||''}" readonly>
|
||||
<input id="caf-pre" class="caf-input" value="${cafEsc(prenomPref)}" readonly>
|
||||
<input id="caf-nom" class="caf-input" value="${cafEsc(nomPref)}" readonly>
|
||||
</div>
|
||||
<input id="caf-mail" class="caf-input" value="${meta.prefill.email||''}" readonly>
|
||||
<input id="caf-mail" class="caf-input" value="${cafEsc(emailPref)}" readonly>
|
||||
</div>`
|
||||
: `<div class="caf-grid" style="margin-bottom:8px">
|
||||
<div class="caf-row">
|
||||
<input id="caf-pre" class="caf-input" placeholder="${L.firstname}">
|
||||
<input id="caf-nom" class="caf-input" placeholder="${L.lastname}">
|
||||
<input id="caf-pre" class="caf-input" placeholder="${cafEsc(L.firstname)}">
|
||||
<input id="caf-nom" class="caf-input" placeholder="${cafEsc(L.lastname)}">
|
||||
</div>
|
||||
<input id="caf-mail" class="caf-input" placeholder="${L.email}" type="email">
|
||||
<input id="caf-mail" class="caf-input" placeholder="${cafEsc(L.email)}" type="email">
|
||||
</div>`;
|
||||
|
||||
// attendre Swal si chargé dynamiquement
|
||||
if (!window.Swal) await new Promise(r => setTimeout(r, 50));
|
||||
// Attendre SweetAlert si chargé dynamiquement
|
||||
if (!window.Swal) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
if (!window.Swal) {
|
||||
throw new Error('SweetAlert2 not loaded');
|
||||
}
|
||||
|
||||
await Swal.fire({
|
||||
title: meta.title,
|
||||
title: cafEsc(meta.title),
|
||||
html: `<div class="caf-body">
|
||||
${identityBlock}
|
||||
<div class="caf-grid">
|
||||
<input id="caf-subj" class="caf-input" placeholder="${L.subject}">
|
||||
<textarea id="caf-msg" class="caf-textarea" rows="5" placeholder="${L.message}"></textarea>
|
||||
<input id="caf-subj" class="caf-input" placeholder="${cafEsc(L.subject)}">
|
||||
<textarea id="caf-msg" class="caf-textarea" rows="5" placeholder="${cafEsc(L.message)}"></textarea>
|
||||
</div>
|
||||
</div>`,
|
||||
showConfirmButton: false,
|
||||
@@ -268,12 +405,15 @@ if (!$IS_DIRECT && $isGet && $isHtmlLike) {
|
||||
didOpen: () => {
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'caf-footer';
|
||||
|
||||
footer.innerHTML = `
|
||||
<button type="button" class="btn-secondary" id="caf-cancel"><?= $h($translations['contact_admin_close']) ?></button>
|
||||
<button type="button" class="btn-primary" id="caf-send">${L.send}</button>`;
|
||||
<button type="button" class="btn-secondary" id="caf-cancel"><?php echo $h($translations['contact_admin_close'] ?? 'Fermer') ?></button>
|
||||
<button type="button" class="btn-primary" id="caf-send">${cafEsc(L.send)}</button>`;
|
||||
|
||||
document.querySelector('.swal2-popup').appendChild(footer);
|
||||
|
||||
document.getElementById('caf-cancel').onclick = () => Swal.close();
|
||||
|
||||
document.getElementById('caf-send').onclick = async () => {
|
||||
const prenom = document.getElementById('caf-pre').value.trim();
|
||||
const nom = document.getElementById('caf-nom').value.trim();
|
||||
@@ -284,11 +424,17 @@ if (!$IS_DIRECT && $isGet && $isHtmlLike) {
|
||||
if (!prenom || !nom || !email || !sujet || !message) {
|
||||
return Swal.showValidationMessage(L.required);
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return Swal.showValidationMessage(L.invalid);
|
||||
}
|
||||
|
||||
if (sujet.length > 180 || message.length > 3000) {
|
||||
return Swal.showValidationMessage(<?php echo json_encode($translations['contact_admin_too_long'] ?? 'Le message est trop long.', JSON_UNESCAPED_UNICODE) ?>);
|
||||
}
|
||||
|
||||
Swal.showLoading();
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('csrf', meta.csrf);
|
||||
fd.set('prenom', prenom);
|
||||
@@ -298,24 +444,36 @@ if (!$IS_DIRECT && $isGet && $isHtmlLike) {
|
||||
fd.set('message', message);
|
||||
|
||||
const resp = await fetch(meta.endpoint, {
|
||||
method:'POST',
|
||||
headers:{'X-Requested-With':'XMLHttpRequest'},
|
||||
method: 'POST',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
body: fd,
|
||||
credentials:'same-origin'
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error('HTTP ' + resp.status);
|
||||
}
|
||||
|
||||
const j = await resp.json();
|
||||
if (j.ok) Swal.fire({ icon:'success', text: j.msg });
|
||||
else Swal.fire({ icon:'error', text: j.msg || <?= json_encode($translations['contact_admin_js_error'], JSON_UNESCAPED_UNICODE) ?> });
|
||||
|
||||
if (j.ok) {
|
||||
Swal.fire({ icon: 'success', text: j.msg });
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
text: j.msg || <?php echo json_encode($translations['contact_admin_js_error'] ?? 'Erreur.', JSON_UNESCAPED_UNICODE) ?>
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}catch(e){
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(<?= json_encode($translations['contact_admin_network_error'], JSON_UNESCAPED_UNICODE) ?>);
|
||||
alert(<?php echo json_encode($translations['contact_admin_network_error'] ?? 'Erreur réseau.', JSON_UNESCAPED_UNICODE) ?>);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
// si inclus dans un contexte non-HTML (JSON/API), ne rien envoyer.
|
||||
}
|
||||
|
||||
// si inclus dans un contexte non-HTML (JSON/API), ne rien envoyer.
|
||||
@@ -2238,4 +2238,9 @@ Cliquez simplement sur le lien pour vous connecter.<br>',
|
||||
'trop_de_tentatives_connexion' => 'Trop de tentatives de connexion. Merci de réessayer un peu plus tard.',
|
||||
'verif_code_trop_tentatives' => 'Trop de tentatives. Veuillez recommencer la procédure de réinitialisation.',
|
||||
'mdp_oublie_message_neutre' => 'Si un compte existe pour cette adresse, un code a été envoyé.',
|
||||
'menu_stats' => 'Statistiques & suivi',
|
||||
'menu_users' => 'Utilisateurs',
|
||||
'menu_communication' => 'Communication',
|
||||
'menu_content' => 'Contenu',
|
||||
'menu_maintenance' => 'Maintenance',
|
||||
];
|
||||
+6
-1
@@ -1468,7 +1468,7 @@ return [
|
||||
'reauth_title' => 'Подтвердите свою личность',
|
||||
'reauth_intro' => 'Чтобы продолжить выполнение чувствительного действия, пожалуйста, введите ваш пароль.',
|
||||
'reauth_password_label' => 'Пароль',
|
||||
'reauth_confirm' => 'Подтвердить',
|
||||
'reauth_confirm' => 'Готово',
|
||||
'reauth_invalid_password' => 'Неверный пароль.',
|
||||
'reauth_csrf_invalid' => 'Недействительный CSRF-токен.',
|
||||
'reauth_footer_notice' => 'Внимание! Усиленная безопасность, ведётся аудит.',
|
||||
@@ -2249,4 +2249,9 @@ return [
|
||||
'trop_de_tentatives_connexion' => 'Слишком много попыток входа. Пожалуйста, попробуйте позже.',
|
||||
'verif_code_trop_tentatives' => 'Слишком много попыток. Пожалуйста, начните процедуру восстановления заново.',
|
||||
'mdp_oublie_message_neutre' => 'Если для этого адреса существует аккаунт, код был отправлен.',
|
||||
'menu_stats' => 'Статистика и контроль',
|
||||
'menu_users' => 'Пользователи',
|
||||
'menu_communication' => 'Коммуникация',
|
||||
'menu_content' => 'Контент',
|
||||
'menu_maintenance' => 'Обслуживание',
|
||||
];
|
||||
+3
-1
@@ -474,6 +474,7 @@
|
||||
if ($page > 1) {
|
||||
$canonicalUrl .= '?page=' . $page;
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
@@ -619,7 +620,8 @@ else: ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="pro-cta-col">
|
||||
<a class="pro-cta" href="public/client/reserver_rdv.php?praticien_id=<?php echo (int) $p['id'] ?>">
|
||||
<a class="pro-cta"
|
||||
href="/public/client/reserver_rdv.php?praticien_id=<?php echo (int) $p['id'] ?>">
|
||||
<?php echo h($translations['den'] ?? 'Выберите день') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user