324 lines
14 KiB
PHP
324 lines
14 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../../public/includes/init_admin.php';
|
|
|
|
// Повторная аутентификация админа (10 минут)
|
|
$ttl = 600;
|
|
if (empty($_SESSION['admin_reauth_at']) || (time() - (int)$_SESSION['admin_reauth_at']) > $ttl) {
|
|
$redir = $_SERVER['REQUEST_URI'] ?? 'index.php?page=afficher_messages';
|
|
header('Location: ?page=reauth&redir='.rawurlencode($redir));
|
|
return;
|
|
}
|
|
|
|
|
|
try { $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (Throwable) {}
|
|
|
|
$e = static fn($s)=>htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
|
|
|
|
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
use PHPMailer\PHPMailer\Exception;
|
|
|
|
// CSRF helper
|
|
if (!function_exists('require_post_csrf')) {
|
|
function require_post_csrf() {
|
|
global $translations;
|
|
if ($_SERVER['REQUEST_METHOD']!=='POST') { http_response_code(405); exit($translations['contactad_method_not_allowed'] ?? 'Méthode non autorisée'); }
|
|
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'])) {
|
|
http_response_code(419); exit($translations['contactad_csrf_invalid'] ?? 'CSRF invalide');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Отправка письма через PHPMailer (HTML + текст)
|
|
function admin_send_reply(string $to, string $subject, string $html, string $text): array {
|
|
/** @var array $MAILCFG */
|
|
global $MAILCFG;
|
|
$m = new PHPMailer(true);
|
|
try {
|
|
$m->isSMTP();
|
|
$m->Host = $MAILCFG['smtp']['host'];
|
|
$m->Port = $MAILCFG['smtp']['port'];
|
|
$m->SMTPSecure = $MAILCFG['smtp']['secure'];
|
|
$m->SMTPAuth = $MAILCFG['smtp']['auth'];
|
|
$m->Username = $MAILCFG['smtp']['username'];
|
|
$m->Password = $MAILCFG['smtp']['password'];
|
|
$m->CharSet = 'UTF-8';
|
|
$m->Encoding = 'base64';
|
|
$m->XMailer = '';
|
|
|
|
if (!empty($MAILCFG['bounce'])) {
|
|
// Envelope From / return-path (для SPF/DMARC)
|
|
$m->Sender = $MAILCFG['bounce'];
|
|
}
|
|
$from = $MAILCFG['from'] ?? ['no-reply@uslugi.fr','USLUGI.FR'];
|
|
$rpto = $MAILCFG['reply_to'] ?? ['no-reply@uslugi.fr','USLUGI.FR'];
|
|
$m->setFrom($from[0], $from[1] ?? '');
|
|
if (!empty($rpto[0])) {
|
|
$m->addReplyTo($rpto[0], $rpto[1] ?? '');
|
|
}
|
|
|
|
$m->addAddress($to);
|
|
|
|
// Анти-автоответы (по желанию)
|
|
if (!empty($MAILCFG['suppress_auto'])) {
|
|
$m->addCustomHeader('Auto-Submitted', 'auto-generated');
|
|
$m->addCustomHeader('X-Auto-Response-Suppress', 'All');
|
|
}
|
|
|
|
$m->isHTML(true);
|
|
$m->Subject = $subject;
|
|
$m->Body = $html;
|
|
$m->AltBody = $text;
|
|
|
|
$m->send();
|
|
return [true, null];
|
|
} catch (Throwable $e) {
|
|
error_log('PHPMailer admin reply error: '.$e->getMessage().($m->ErrorInfo ? ' | '.$m->ErrorInfo : ''));
|
|
return [false, $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
// Действия POST: ответ / удаление
|
|
$err = null;
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
require_post_csrf();
|
|
$action = $_POST['action'] ?? '';
|
|
|
|
if ($action === 'reply') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$txt = trim((string)($_POST['reply'] ?? ''));
|
|
if ($id <= 0 || $txt === '') {
|
|
$err = $translations['contactad_err_invalid_data'] ?? 'Données invalides';
|
|
} else {
|
|
$st = $pdo->prepare("SELECT * FROM contact_messages WHERE id=? LIMIT 1");
|
|
$st->execute([$id]);
|
|
$m = $st->fetch(PDO::FETCH_ASSOC);
|
|
if (!$m) {
|
|
$err = $translations['contactad_err_not_found'] ?? 'Message introuvable';
|
|
} else {
|
|
$to = (string)$m['email'];
|
|
$subj = $translations['contactad_mail_subject_reply'] ?? "Réponse de l'administrateur";
|
|
$greet= $translations['contactad_mail_greeting'] ?? "Bonjour";
|
|
$orig = $translations['contactad_mail_original_label'] ?? "Votre message initial :";
|
|
|
|
// HTML версия
|
|
$origHtml = nl2br($e($m['message']));
|
|
$html = '
|
|
<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111;">
|
|
<p>'.$e($greet).' '.$e($m['prenom'].' '.$m['nom']).',</p>
|
|
<p>'.$e($txt).'</p>
|
|
<hr>
|
|
<p><strong>'.$e($orig).'</strong></p>
|
|
<blockquote style="margin:0;padding:10px 12px;border-left:4px solid #ddd;background:#f7f7f7;">'.$origHtml.'</blockquote>
|
|
</div>';
|
|
|
|
// Текстовая версия
|
|
$text = $greet.' '.($m['prenom'].' '.$m['nom']).","."\n\n".
|
|
$txt."\n\n".
|
|
"-----------------------\n".$orig."\n".$m['message']."\n";
|
|
|
|
// Отправка
|
|
[$ok,$errSend] = admin_send_reply($to, $subj, $html, $text);
|
|
|
|
// Записываем статус "repondu" в любом случае (как раньше)
|
|
$stU = $pdo->prepare("UPDATE contact_messages
|
|
SET statut='repondu', reponse=?, replied_by=?, replied_at=NOW()
|
|
WHERE id=?");
|
|
$stU->execute([$txt, (int)($_SESSION['compte_id'] ?? 0), $id]);
|
|
|
|
if (function_exists('log_admin_action')) {
|
|
log_admin_action(
|
|
$pdo,
|
|
(int)($_SESSION['compte_id'] ?? 0),
|
|
'contact_reply',
|
|
json_encode(['id'=>$id,'email'=>$to,'sent'=>(bool)$ok,'error'=>$errSend], JSON_UNESCAPED_UNICODE)
|
|
);
|
|
}
|
|
|
|
// Возврат к списку (без id)
|
|
header('Location: ?page=afficher_messages&flash=1');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
elseif ($action === 'delete') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
if ($id > 0) {
|
|
$del = $pdo->prepare("DELETE FROM contact_messages WHERE id=?");
|
|
$del->execute([$id]);
|
|
if (function_exists('log_admin_action')) {
|
|
log_admin_action($pdo, (int)($_SESSION['compte_id'] ?? 0), 'contact_delete',
|
|
json_encode(['id'=>$id,'deleted'=>$del->rowCount()>0], JSON_UNESCAPED_UNICODE));
|
|
}
|
|
header('Location: ?page=afficher_messages&deleted=1'); return;
|
|
} else {
|
|
$err = $translations['contactad_err_invalid_id'] ?? 'ID invalide';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Открытое сообщение?
|
|
$msg = null;
|
|
if (isset($_GET['id'])) {
|
|
$id = (int)$_GET['id'];
|
|
$st = $pdo->prepare("SELECT * FROM contact_messages WHERE id=? LIMIT 1");
|
|
$st->execute([$id]);
|
|
$msg = $st->fetch(PDO::FETCH_ASSOC);
|
|
if ($msg && $msg['statut'] === 'nouveau') {
|
|
$pdo->prepare("UPDATE contact_messages SET statut='lu' WHERE id=?")->execute([$id]);
|
|
$msg['statut'] = 'lu';
|
|
}
|
|
}
|
|
|
|
// Список (пагинация)
|
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
|
$per = 25; $off = ($page - 1) * $per;
|
|
$total = (int)$pdo->query("SELECT COUNT(*) FROM contact_messages")->fetchColumn();
|
|
$rows = $pdo->query("SELECT id, nom, prenom, email, sujet, statut, created_at
|
|
FROM contact_messages
|
|
ORDER BY id DESC
|
|
LIMIT $per OFFSET $off")->fetchAll(PDO::FETCH_ASSOC);
|
|
$pages = max(1, (int)ceil($total / $per));
|
|
?>
|
|
<div class="shadow p-4 rounded bg-light">
|
|
<div class="container my-4">
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<h3 class="m-0"><?= $e($translations['contactad_title'] ?? 'Messages utilisateurs') ?></h3>
|
|
<span class="badge text-bg-secondary"><?= (int)$total ?></span>
|
|
<!-- Переключатель языков удалён -->
|
|
</div>
|
|
|
|
<?php if(isset($_GET['flash'])): ?>
|
|
<div class="alert alert-success"><?= $e($translations['contactad_flash_sent_ok'] ?? 'Réponse envoyée') ?></div>
|
|
<?php endif; ?>
|
|
<?php if(isset($_GET['deleted'])): ?>
|
|
<div class="alert alert-success"><?= $e($translations['contactad_flash_deleted_ok'] ?? 'Message supprimé.') ?></div>
|
|
<?php endif; ?>
|
|
<?php if($err): ?><div class="alert alert-danger"><?= $e($err) ?></div><?php endif; ?>
|
|
|
|
<div class="row g-3">
|
|
<!-- Список: на весь экран если деталка не открыта -->
|
|
<div class="<?= $msg ? 'col-lg-6' : 'col-12' ?>">
|
|
<div class="card h-100">
|
|
<div class="card-header"><?= $e($translations['contactad_list_title'] ?? 'Messages') ?></div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0 align-middle mobile-cards">
|
|
<thead>
|
|
<tr>
|
|
<th><?= $e($translations['contactad_th_id'] ?? '#') ?></th>
|
|
<th><?= $e($translations['contactad_th_from'] ?? 'De') ?></th>
|
|
<th><?= $e($translations['contactad_th_email'] ?? 'Email') ?></th>
|
|
<th><?= $e($translations['contactad_th_subject'] ?? 'Sujet') ?></th>
|
|
<th><?= $e($translations['contactad_th_date'] ?? 'Date') ?></th>
|
|
<th class="text-end"><?= $e($translations['contactad_th_actions'] ?? 'Actions') ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($rows as $r): $st=$r['statut']; ?>
|
|
<tr>
|
|
<td><?= (int)$r['id'] ?></td>
|
|
<td><?= $e($r['prenom'].' '.$r['nom']) ?></td>
|
|
<td><a href="mailto:<?= $e($r['email']) ?>"><?= $e($r['email']) ?></a></td>
|
|
<td>
|
|
<?= $e($r['sujet']) ?>
|
|
<span class="badge ms-2 text-bg-<?= $st==='nouveau'?'warning':($st==='repondu'?'success':'secondary') ?>">
|
|
<?php
|
|
$label = $st;
|
|
if ($st === 'nouveau') $label = $translations['contactad_status_nouveau'] ?? 'Nouveau';
|
|
elseif ($st === 'lu') $label = $translations['contactad_status_lu'] ?? 'Lu';
|
|
elseif ($st === 'repondu') $label = $translations['contactad_status_repondu'] ?? 'Répondu';
|
|
echo $e($label);
|
|
?>
|
|
</span>
|
|
</td>
|
|
<td><small><?= $e($r['created_at']) ?></small></td>
|
|
<td class="text-end">
|
|
<!-- Кнопки выровнены: d-flex + gap-2, форма без внешних отступов -->
|
|
<div class="d-flex justify-content-end gap-2">
|
|
<a class="btn btn-sm btn-outline-secondary"
|
|
href="?page=afficher_messages&id=<?= (int)$r['id'] ?>">
|
|
<?= $e($translations['contactad_btn_open'] ?? 'Ouvrir') ?>
|
|
</a>
|
|
<form method="post" class="m-0" onsubmit="return confirm('<?= $e($translations['contactad_confirm_delete'] ?? 'Supprimer ce message ?') ?>')">
|
|
<input type="hidden" name="csrf_token" value="<?= $e($_SESSION['csrf_token'] ?? '') ?>">
|
|
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
|
<button class="btn btn-sm btn-outline-danger" name="action" value="delete">
|
|
<?= $e($translations['contactad_btn_delete'] ?? 'Supprimer') ?>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; if (!$rows): ?>
|
|
<tr><td colspan="6" class="p-3 text-muted"><?= $e($translations['contactad_no_messages'] ?? 'Aucun message.') ?></td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if ($pages > 1): ?>
|
|
<div class="card-footer d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<?php
|
|
$tpl = $translations['contactad_pager_label'] ?? 'Page {P} / {N}';
|
|
echo $e(str_replace(['{P}','{N}'], [(string)(int)$page, (string)(int)$pages], $tpl));
|
|
?>
|
|
</div>
|
|
<div class="btn-group">
|
|
<?php for ($i=max(1,$page-2); $i<=min($pages,$page+2); $i++):
|
|
$q=$_GET; $q['p']=$i; ?>
|
|
<a class="btn btn-sm <?= $i===$page?'btn-dark':'btn-outline-dark' ?>" href="?<?= http_build_query($q) ?>"><?= $i ?></a>
|
|
<?php endfor; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($msg): ?>
|
|
<div class="col-lg-6">
|
|
<div class="card h-100">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span><?= $e($translations['contactad_detail_title'] ?? 'Détails') ?></span>
|
|
<div class="d-flex gap-2">
|
|
<a class="btn btn-sm btn-outline-secondary" href="?page=afficher_messages">
|
|
<?= $e($translations['contactad_btn_close_detail'] ?? 'Fermer') ?>
|
|
</a>
|
|
<form method="post" class="m-0" onsubmit="return confirm('<?= $e($translations['contactad_confirm_delete'] ?? 'Supprimer ce message ?') ?>')">
|
|
<input type="hidden" name="csrf_token" value="<?= $e($_SESSION['csrf_token'] ?? '') ?>">
|
|
<input type="hidden" name="id" value="<?= (int)$msg['id'] ?>">
|
|
<button class="btn btn-sm btn-danger" name="action" value="delete"><?= $e($translations['contactad_btn_delete'] ?? 'Supprimer') ?></button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-2">
|
|
<strong>#<?= (int)$msg['id'] ?></strong> • <?= $e($msg['created_at']) ?>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong><?= $e($msg['prenom'].' '.$msg['nom']) ?></strong> —
|
|
<a href="mailto:<?= $e($msg['email']) ?>"><?= $e($msg['email']) ?></a>
|
|
</div>
|
|
<div class="mb-2"><em><?= $e($msg['sujet']) ?></em></div>
|
|
<pre class="p-2 bg-light border rounded" style="white-space:pre-wrap"><?= $e($msg['message']) ?></pre>
|
|
|
|
<hr>
|
|
<form method="post" class="d-grid gap-2">
|
|
<input type="hidden" name="csrf_token" value="<?= $e($_SESSION['csrf_token'] ?? '') ?>">
|
|
<input type="hidden" name="id" value="<?= (int)$msg['id'] ?>">
|
|
<textarea class="form-control" name="reply" rows="5"
|
|
placeholder="<?= $e($translations['contactad_reply_placeholder'] ?? 'Répondre') ?>"
|
|
required><?= $e($msg['reponse'] ?? '') ?></textarea>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-dark" name="action" value="reply"><?= $e($translations['contactad_btn_send'] ?? 'Envoyer') ?></button>
|
|
<a class="btn btn-outline-secondary" href="?page=afficher_messages"><?= $e($translations['contactad_btn_back'] ?? 'Retour') ?></a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|