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']) . ' :
' + . '' + . '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(); + ?> +Ce lien de confirmation n'est plus valide (expiré après 24 h) ou a déjà été utilisé.
+ ← Retour à l'accueil +Aucun commentaire pour l'instant.
+ +| Article | +Auteur | +Commentaire | +Date | +État | ++ |
|---|---|---|---|---|---|
| + + = htmlspecialchars($cSlug) ?> + + = htmlspecialchars(substr($c['article_uuid'], 0, 8)) ?>… + + | +
+ = htmlspecialchars($c['author_name']) ?>
+ = htmlspecialchars($c['author_email']) ?>
+ |
+
+
+ = htmlspecialchars(mb_strimwidth($c['content'], 0, 80, '…')) ?>
+
+
+ Code : = htmlspecialchars($c['verification_code']) ?> + + |
+ = htmlspecialchars($cDate) ?> | += $cVerif ?> = $cPub ?> | ++ + + + + + | +