feat: déplacer 'À lire aussi' après les réactions dans la colonne principale

This commit is contained in:
Cedric Abonnel
2026-05-13 01:32:03 +02:00
parent 0a44ab9da2
commit 78d6c656be
7 changed files with 355 additions and 21 deletions
+212 -1
View File
@@ -22,7 +22,7 @@ $action = $_GET['action'] ?? 'list';
$uuid = $_GET['uuid'] ?? '';
$slug = $_GET['slug'] ?? '';
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links'];
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links', 'react', 'comment', 'verify_comment', 'comment_moderate'];
$metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' : null;
unset($_noindexActions);
@@ -635,6 +635,38 @@ switch ($action) {
$backlinks = $articles->getBacklinks($article['slug'] ?? '', $article['uuid']);
// Réactions et commentaires
require_once BASE_PATH . '/src/ReactionManager.php';
require_once BASE_PATH . '/src/CommentManager.php';
$reactionStats = array_fill_keys(ReactionManager::TYPES, 0);
$visitorReactions = [];
$comments = [];
$commentFlash = isset($_GET['commented']);
$commentVerified = isset($_GET['verified']);
$commentError = null;
if ($pdo) {
$reactionMgr = new ReactionManager($pdo);
$commentMgr = new CommentManager($pdo);
// Cookie visiteur (fingerprint anti-doublon)
if (empty($_COOKIE['vl_vid'])) {
$vid = bin2hex(random_bytes(16));
setcookie('vl_vid', $vid, [
'expires' => time() + 365 * 86400,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax',
]);
} else {
$vid = $_COOKIE['vl_vid'];
}
$reactionStats = $reactionMgr->statsForArticle($article['uuid']);
$visitorReactions = $reactionMgr->visitorReactions($article['uuid'], $vid);
$comments = $commentMgr->forArticle($article['uuid']);
}
include BASE_PATH . '/templates/post_view.php';
break;
@@ -1616,6 +1648,161 @@ switch ($action) {
header('Location: /edit/' . rawurlencode($uuid));
exit;
case 'react':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /');
exit;
}
$reactUuid = trim($_POST['uuid'] ?? '');
$reactType = trim($_POST['type'] ?? '');
$isAjax = ($_POST['_ajax'] ?? '') === '1';
// Cookie visiteur
if (empty($_COOKIE['vl_vid'])) {
$vid = bin2hex(random_bytes(16));
setcookie('vl_vid', $vid, [
'expires' => time() + 365 * 86400,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax',
]);
} else {
$vid = $_COOKIE['vl_vid'];
}
$pdo = dbPdo();
if ($pdo && $reactUuid !== '') {
require_once BASE_PATH . '/src/ReactionManager.php';
$rm = new ReactionManager($pdo);
$added = $rm->toggle($reactUuid, $reactType, $vid);
$count = $rm->statsForArticle($reactUuid)[$reactType] ?? 0;
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['ok' => true, 'active' => $added, 'count' => $count]);
exit;
}
}
$reactBack = $_POST['_back'] ?? '/';
header('Location: ' . $reactBack);
exit;
case 'comment':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /');
exit;
}
// Honeypot
if (($_POST['website'] ?? '') !== '') {
header('Location: /');
exit;
}
// CSRF
$csrfOk = isset($_POST['_token'], $_SESSION['comment_csrf'])
&& hash_equals($_SESSION['comment_csrf'], $_POST['_token']);
unset($_SESSION['comment_csrf']);
if (!$csrfOk) {
header('Location: /');
exit;
}
$cmtUuid = trim($_POST['uuid'] ?? '');
$cmtName = trim($_POST['author_name'] ?? '');
$cmtEmail = trim($_POST['author_email'] ?? '');
$cmtContent = trim($_POST['content'] ?? '');
$cmtArticle = $cmtUuid !== '' ? $articles->getByUuid($cmtUuid) : null;
$cmtBack = $cmtArticle ? '/post/' . rawurlencode($cmtArticle['slug'] ?? $cmtUuid) : '/';
$pdo = dbPdo();
if (!$pdo || !$cmtArticle || $cmtName === '' || !filter_var($cmtEmail, FILTER_VALIDATE_EMAIL) || $cmtContent === '') {
header('Location: ' . $cmtBack . '#comment-form-card');
exit;
}
if (mb_strlen($cmtContent) > 2000) {
header('Location: ' . $cmtBack . '#comment-form-card');
exit;
}
require_once BASE_PATH . '/src/CommentManager.php';
require_once BASE_PATH . '/src/mailer.php';
$cm = new CommentManager($pdo);
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$code = $cm->submit($cmtUuid, $cmtName, $cmtEmail, $cmtContent, $ip, $ua);
$verifyUrl = rtrim(APP_URL, '/') . '/verify-comment/' . $code;
$articleUrl = rtrim(APP_URL, '/') . $cmtBack;
$subject = '[' . siteTitle() . '] Confirmez votre commentaire';
$html = '<!DOCTYPE html><html><body style="font-family:sans-serif;max-width:560px;margin:0 auto">'
. '<p>Bonjour ' . htmlspecialchars($cmtName) . ',</p>'
. '<p>Cliquez sur le bouton ci-dessous pour confirmer votre commentaire sur <em>' . htmlspecialchars($cmtArticle['title']) . '</em> :</p>'
. '<p><a href="' . htmlspecialchars($verifyUrl) . '" style="display:inline-block;padding:10px 20px;background:#0d6efd;color:#fff;text-decoration:none;border-radius:4px">Confirmer mon commentaire</a></p>'
. '<p style="color:#888;font-size:.875em">Ou copiez ce lien dans votre navigateur :<br>' . htmlspecialchars($verifyUrl) . '</p>'
. '<p style="color:#888;font-size:.875em">Ce lien expire dans 24 heures. Si vous n\'êtes pas à l\'origine de ce message, ignorez-le.</p>'
. '</body></html>';
try {
envoyer_mail_smtp($cmtEmail, $subject, $html);
} catch (\RuntimeException) {
// Taux limité ou erreur SMTP : on continue sans planter le visiteur
}
header('Location: ' . $cmtBack . '?commented=1#comments');
exit;
case 'verify_comment':
$vcCode = trim($_GET['code'] ?? '');
$pdo = dbPdo();
if ($pdo && preg_match('/^[0-9]{6}$/', $vcCode)) {
require_once BASE_PATH . '/src/CommentManager.php';
$cm = new CommentManager($pdo);
$vcUuid = $cm->verify($vcCode);
if ($vcUuid !== null) {
$vcArticle = $articles->getByUuid($vcUuid);
$vcSlug = $vcArticle ? ($vcArticle['slug'] ?? $vcUuid) : $vcUuid;
header('Location: /post/' . rawurlencode($vcSlug) . '?verified=1#comments');
exit;
}
}
// Code invalide ou expiré
http_response_code(404);
ob_start();
?>
<div class="container py-5 text-center">
<h1 class="h2 mb-3">Lien invalide ou expiré</h1>
<p class="text-muted mb-4">Ce lien de confirmation n'est plus valide (expiré après 24 h) ou a déjà été utilisé.</p>
<a href="/" class="btn btn-primary">← Retour à l'accueil</a>
</div>
<?php
$content = ob_get_clean();
$title = 'Lien invalide — ' . siteTitle();
$metaRobots = 'noindex, nofollow';
include BASE_PATH . '/templates/layout.php';
break;
case 'comment_moderate':
requireAuth();
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(403);
exit;
}
$modId = (int)($_POST['id'] ?? 0);
$modPub = (int)($_POST['pub'] ?? 1);
$pdo = dbPdo();
if ($pdo && $modId > 0) {
require_once BASE_PATH . '/src/CommentManager.php';
(new CommentManager($pdo))->setPublished($modId, $modPub === 1);
}
header('Location: /admin/comments');
exit;
case 'rate':
requireAuth();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
@@ -1780,6 +1967,30 @@ switch ($action) {
}
}
if ($tab === 'comments') {
if (!isAdmin()) {
http_response_code(403);
exit;
}
$pdo = dbPdo();
if ($pdo) {
require_once BASE_PATH . '/src/CommentManager.php';
$adminData['comments'] = (new CommentManager($pdo))->allForAdmin();
// Enrichit avec le slug de chaque article pour les liens
$adminData['articleSlugs'] = [];
foreach ($adminData['comments'] as $cmtRow) {
$uuid = $cmtRow['article_uuid'];
if (!isset($adminData['articleSlugs'][$uuid])) {
$a = $articles->getByUuid($uuid);
$adminData['articleSlugs'][$uuid] = $a ? ($a['slug'] ?? null) : null;
}
}
} else {
$adminData['comments'] = [];
$adminData['articleSlugs'] = [];
}
}
include BASE_PATH . '/templates/admin.php';
break;