Files

479 lines
17 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[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.