V10.1 stable avec correction securité,bug sur csrf dans index

This commit is contained in:
Oleg Barykin committed 2026-05-27 13:54:59 +02:00
1 parent e9d0010451
commit 21f34c64c9
11 files changed
+1136 -445

No files matched your search

+352 -194
View File
@@ -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 {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[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.
+5
View File
@@ -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
View File
@@ -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' => 'Обслуживание',
];