From 78d6c656be197fc702a58131aecd5b3c033d04a2 Mon Sep 17 00:00:00 2001 From: Cedric Abonnel Date: Wed, 13 May 2026 01:32:03 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20d=C3=A9placer=20'=C3=80=20lire=20aussi'?= =?UTF-8?q?=20apr=C3=A8s=20les=20r=C3=A9actions=20dans=20la=20colonne=20pr?= =?UTF-8?q?incipale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/.htaccess | 6 + public/assets/css/style.css | 30 +++++ public/index.php | 213 ++++++++++++++++++++++++++++++++- templates/admin.php | 77 ++++++++++++ templates/comments_section.php | 25 ++++ templates/layout.php | 3 + templates/post_view.php | 22 +--- 7 files changed, 355 insertions(+), 21 deletions(-) diff --git a/public/.htaccess b/public/.htaccess index 359b71d..79b6a24 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -36,6 +36,12 @@ RewriteRule ^admin/role/([a-z0-9_-]+)/?$ /index.php?action=admin_role_edit&role_ RewriteRule ^admin/([a-z0-9-]+)/?$ /index.php?action=admin&tab=$1 [L,QSA] RewriteRule ^admin/?$ /index.php?action=admin [L,QSA] +# Réactions et commentaires +RewriteRule ^react/?$ /index.php?action=react [L,QSA] +RewriteRule ^comment/?$ /index.php?action=comment [L,QSA] +RewriteRule ^comment-moderate/?$ /index.php?action=comment_moderate [L,QSA] +RewriteRule ^verify-comment/([0-9]{6})/?$ /index.php?action=verify_comment&code=$1 [L,QSA] + # Pages de gestion RewriteRule ^categories/?$ /index.php?action=categories [L,QSA] RewriteRule ^profile/?$ /index.php?action=profile [L,QSA] diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 4e337ec..f5978c7 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -681,6 +681,36 @@ textarea.form-control { border-color: var(--vl-accent); } +.also-read-title { + font-size: .7rem; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--vl-muted); + margin-bottom: .75rem; +} + +.also-read-grid { + display: flex; + flex-wrap: wrap; + gap: .75rem; +} + +.also-read-grid .related-card { + flex: 1 1 200px; + max-width: 280px; + background: var(--vl-surface); + border-radius: var(--vl-radius); + padding: .65rem; + box-shadow: var(--vl-shadow-sm); + transition: box-shadow .15s, transform .15s; +} + +.also-read-grid .related-card:hover { + box-shadow: var(--vl-shadow-md); + transform: translateY(-1px); +} + .related-card { display: flex; gap: 0.75rem; diff --git a/public/index.php b/public/index.php index 11b4809..1e5954a 100644 --- a/public/index.php +++ b/public/index.php @@ -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 = '' + . '

Bonjour ' . htmlspecialchars($cmtName) . ',

' + . '

Cliquez sur le bouton ci-dessous pour confirmer votre commentaire sur ' . htmlspecialchars($cmtArticle['title']) . ' :

' + . '

Confirmer mon commentaire

' + . '

Ou copiez ce lien dans votre navigateur :
' . htmlspecialchars($verifyUrl) . '

' + . '

Ce lien expire dans 24 heures. Si vous n\'êtes pas à l\'origine de ce message, ignorez-le.

' + . ''; + + 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(); + ?> +
+

Lien invalide ou expiré

+

Ce lien de confirmation n'est plus valide (expiré après 24 h) ou a déjà été utilisé.

+ ← Retour à l'accueil +
+ 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; diff --git a/templates/admin.php b/templates/admin.php index 39bc278..fa5cef0 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -48,6 +48,10 @@ function adminStatusBadge(array $a, int $now): string Site + @@ -461,6 +465,79 @@ function adminStatusBadge(array $a, int $now): string + + + +
Commentaires
+ +

Aucun commentaire pour l'instant.

+ +
+ + + + + + + + + + + + + + Vérifié' : 'En attente'; + $cPub = $c['published'] ? 'Publié' : 'Masqué'; + ?> + + + + + + + + + + +
ArticleAuteurCommentaireDateÉtat
+ + + + + + +
+
+
+ + + + +
Code : + +
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + + + + + +
+
À lire aussi
+ +
+ +
diff --git a/templates/layout.php b/templates/layout.php index 29d1482..1cdbee8 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -131,6 +131,9 @@ $_layoutCurrentCat = trim($_GET['cat'] ?? ''); + + + diff --git a/templates/post_view.php b/templates/post_view.php index dce4463..03e3bf0 100644 --- a/templates/post_view.php +++ b/templates/post_view.php @@ -170,6 +170,8 @@ $hasSources = (!empty($externalLinks) || !empty($files))

+ +
@@ -286,26 +288,6 @@ $hasSources = (!empty($externalLinks) || !empty($files))
- - - - - - - - -