479 lines
17 KiB
PHP
479 lines
17 KiB
PHP
<?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.
|
||
|
||
declare (strict_types = 1);
|
||
|
||
$IS_DIRECT = isset($_SERVER['SCRIPT_FILENAME'])
|
||
&& realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME']);
|
||
|
||
require_once __DIR__ . '/../../includes/bootstrap_global.php';
|
||
global $pdo;
|
||
|
||
if (! isset($pdo) || ! ($pdo instanceof PDO)) {
|
||
return;
|
||
}
|
||
/* ======================= 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;
|
||
};
|
||
|
||
/* ======================= CSRF ======================= */
|
||
if (empty($_SESSION['pub_csrf'])) {
|
||
$_SESSION['pub_csrf'] = bin2hex(random_bytes(32));
|
||
}
|
||
$CSRF = $_SESSION['pub_csrf'];
|
||
|
||
/* ======================= 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'],
|
||
];
|
||
}
|
||
} catch (Throwable $e) {}
|
||
}
|
||
|
||
$roleAllowed = $isLogged && in_array((string) $user['role'], $ALLOWED_ROLES, true);
|
||
|
||
/* ======================= 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();
|
||
};
|
||
|
||
/* ======================= DIRECT: POST handler ======================= */
|
||
if ($IS_DIRECT && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
|
||
$out = ['ok' => false, 'msg' => ''];
|
||
|
||
try {
|
||
if (! $roleAllowed) {
|
||
throw new Exception('FORBIDDEN');
|
||
}
|
||
|
||
// Anti-spam simple : 10 secondes
|
||
$last = (int) ($_SESSION['last_contact_at'] ?? 0);
|
||
if (time() - $last < 10) {
|
||
throw new Exception('TOO_MANY');
|
||
}
|
||
|
||
// 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];
|
||
});
|
||
}
|
||
|
||
// Charge SweetAlert2 si absent
|
||
(function(){
|
||
if (!window.Swal) {
|
||
var s = document.createElement('script');
|
||
s.src = "https://cdn.jsdelivr.net/npm/sweetalert2@11";
|
||
s.defer = true;
|
||
document.head.appendChild(s);
|
||
}
|
||
})();
|
||
|
||
// Inject style une seule fois
|
||
(function(){
|
||
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}
|
||
.caf-body{padding:18px 22px}
|
||
.caf-grid{display:grid;gap:10px}
|
||
.caf-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||
.caf-input,.caf-textarea{
|
||
width:100%;border:1px solid #e5e7eb;border-radius:12px;padding:10px 12px;font-size:14px;outline:none;
|
||
}
|
||
.caf-input:focus,.caf-textarea:focus{border-color:#38378B;box-shadow:0 0 0 3px rgba(56,55,139,.12)}
|
||
.caf-footer{display:flex;gap:10px;justify-content:flex-end;padding:14px 18px;border-top:1px solid #eee}
|
||
.btn-primary{
|
||
background:#38378B;color:#fff;border:none;border-radius:999px;padding:10px 14px;font-weight:600;cursor:pointer
|
||
}
|
||
.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);
|
||
}
|
||
})();
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const btn = document.getElementById('open-contact');
|
||
|
||
if (btn) {
|
||
btn.addEventListener('click', function(e){
|
||
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'
|
||
});
|
||
|
||
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 || <?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="${cafEsc(prenomPref)}" readonly>
|
||
<input id="caf-nom" class="caf-input" value="${cafEsc(nomPref)}" readonly>
|
||
</div>
|
||
<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="${cafEsc(L.firstname)}">
|
||
<input id="caf-nom" class="caf-input" placeholder="${cafEsc(L.lastname)}">
|
||
</div>
|
||
<input id="caf-mail" class="caf-input" placeholder="${cafEsc(L.email)}" type="email">
|
||
</div>`;
|
||
|
||
// 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: cafEsc(meta.title),
|
||
html: `<div class="caf-body">
|
||
${identityBlock}
|
||
<div class="caf-grid">
|
||
<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,
|
||
showCancelButton: false,
|
||
didOpen: () => {
|
||
const footer = document.createElement('div');
|
||
footer.className = 'caf-footer';
|
||
|
||
footer.innerHTML = `
|
||
<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();
|
||
const email = document.getElementById('caf-mail').value.trim();
|
||
const sujet = document.getElementById('caf-subj').value.trim();
|
||
const message = document.getElementById('caf-msg').value.trim();
|
||
|
||
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);
|
||
fd.set('nom', nom);
|
||
fd.set('email', email);
|
||
fd.set('sujet', sujet);
|
||
fd.set('message', message);
|
||
|
||
const resp = await fetch(meta.endpoint, {
|
||
method: 'POST',
|
||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||
body: fd,
|
||
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 || <?php echo json_encode($translations['contact_admin_js_error'] ?? 'Erreur.', JSON_UNESCAPED_UNICODE) ?>
|
||
});
|
||
}
|
||
};
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.error(e);
|
||
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.
|