Files
maria_uslugi/admin/tools/afficher_messages.php
T
2026-03-05 10:25:39 +01:00

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>