From 6d3e33ceb71c0c55feacfd65b2313cd6df93efe4 Mon Sep 17 00:00:00 2001 From: Emidio Reggiani Date: Tue, 25 Nov 2025 18:41:15 +0100 Subject: [PATCH] Improved comments file naming, handled corresponding markdown file rename and delete. Introduced pagination in admin comments list. Capability to show comments in posts, static, authors. --- config/comments.ini.example | 33 +++++ system/admin/admin.php | 163 ++++++++++++++++++++- system/admin/views/comments.html.php | 36 ++++- system/admin/views/layout.html.php | 4 +- system/htmly.php | 73 +++++++--- system/includes/comments-frontend.php | 30 ++-- system/includes/comments.php | 197 +++++++++++++++++++++----- system/includes/dispatch.php | 4 +- 8 files changed, 456 insertions(+), 84 deletions(-) create mode 100644 config/comments.ini.example diff --git a/config/comments.ini.example b/config/comments.ini.example new file mode 100644 index 0000000..a40bab9 --- /dev/null +++ b/config/comments.ini.example @@ -0,0 +1,33 @@ +; HTMLy Local Comments System Configuration +; To enable local comments, set comment.system = "local" in config/config.ini + +; Require admin moderation before publishing comments +comments.moderation = "true" + +; Enable honeypot anti-spam protection +comments.honeypot = "true" + +; Enable email notifications +comments.notify = "true" + +; Show comments section in posts +comments.show.posts = "true" + +; Show comments section in static pages +comments.show.static = "false" + +; Show comments section in authors pages +comments.show.author = "false" + +; Admin email for comment notifications +comments.admin.email = "your@email.here" + +; Email notification settings (using PHPMailer) +comments.mail.enabled = "true" +comments.mail.host = "mail.host.com" +comments.mail.port = "587" +comments.mail.username = "your_username" +comments.mail.password = "your_password*" +comments.mail.encryption = "tls" +comments.mail.from.email = "your@email.here" +comments.mail.from.name = "Your From Name" diff --git a/system/admin/admin.php b/system/admin/admin.php index fb081d6..07e6847 100644 --- a/system/admin/admin.php +++ b/system/admin/admin.php @@ -255,9 +255,10 @@ function add_content($title, $tag, $url, $content, $user, $draft, $category, $ty $newfile = $dir . $filename; if ($oldfile !== $newfile && !is_null($autoSave)) { if (file_exists($oldfile)) { - + rename($oldfile, $newfile); - + rename_comments($oldfile, $newfile); + if (config('fulltext.search') == "true") { if (file_exists($searchFile)) { @@ -455,6 +456,7 @@ function edit_content($title, $tag, $url, $content, $oldfile, $revertPost, $publ file_put_contents($newfile, print_r($post_content, true), LOCK_EX); unlink($oldfile); + rename_comments($oldfile, $newfile); } else { @@ -474,7 +476,14 @@ function edit_content($title, $tag, $url, $content, $oldfile, $revertPost, $publ file_put_contents($oldfile, print_r($post_content, true), LOCK_EX); } else { rename($oldfile, $newfile); + rename_comments($oldfile, $newfile); file_put_contents($newfile, print_r($post_content, true), LOCK_EX); + + $oldcommentsfile = get_comments_file_from_md($oldfile); + if (file_exists($oldcommentsfile)) { + $newcommentsfile = get_comments_file_from_md($newfile); + rename($oldcommentsfile, $newcommentsfile); + } } } else { @@ -490,11 +499,10 @@ function edit_content($title, $tag, $url, $content, $oldfile, $revertPost, $publ file_put_contents($newfile, print_r($post_content, true), LOCK_EX); unlink($oldfile); - + rename_comments($oldfile, $newfile); + } } - } - if(!empty($publishDraft)) { $dt = $olddate; $t = str_replace('-', '', $dt); @@ -672,6 +680,7 @@ function add_page($title, $url, $content, $draft, $description = null, $autoSave if ($oldfile !== $newfile && !is_null($autoSave)) { if (file_exists($oldfile)) { rename($oldfile, $newfile); + rename_comments($oldfile, $newfile); } } file_put_contents($newfile, print_r($post_content, true), LOCK_EX); @@ -768,6 +777,7 @@ function add_sub_page($title, $url, $content, $static, $draft, $description = nu if ($oldfile !== $newfile && !is_null($autoSave)) { if (file_exists($oldfile)) { rename($oldfile, $newfile); + rename_comments($oldfile, $newfile); } } file_put_contents($newfile, print_r($post_content, true), LOCK_EX); @@ -838,6 +848,8 @@ function edit_page($title, $url, $content, $oldfile, $revertPage, $publishDraft, } } unlink($oldfile); + rename_comments($oldfile, $newfile); + } elseif (!empty($publishDraft)) { $newfile = dirname($dir) . '/' . $post_url . '.md'; file_put_contents($newfile, print_r($post_content, true), LOCK_EX); @@ -848,12 +860,15 @@ function edit_page($title, $url, $content, $oldfile, $revertPage, $publishDraft, } } unlink($oldfile); + rename_comments($oldfile, $newfile); + } else { $newfile = $dir . '/' . $post_url . '.md'; if ($oldfile === $newfile) { file_put_contents($oldfile, print_r($post_content, true), LOCK_EX); } else { rename($oldfile, $newfile); + rename_comments($oldfile, $newfile); file_put_contents($newfile, print_r($post_content, true), LOCK_EX); if (empty($static)) { $old = pathinfo($oldfile, PATHINFO_FILENAME); @@ -1011,6 +1026,7 @@ function edit_category($title, $url, $content, $oldfile, $destination = null, $d } else { if (file_exists($oldfile)) { rename($oldfile, $newfile); + rename_comments($oldfile, $newfile); file_put_contents($newfile, print_r($post_content, true), LOCK_EX); } else { file_put_contents($newfile, print_r($post_content, true), LOCK_EX); @@ -1147,6 +1163,7 @@ function delete_post($file, $destination) if (!empty($deleted_content)) { if ($user === $arr[1] || $role === 'editor' || $role === 'admin') { unlink($deleted_content); + delete_comments($deleted_content); rebuilt_cache('all'); if ($destination == 'post') { $redirect = site_url(); @@ -1199,6 +1216,7 @@ function delete_page($file, $destination) if (!empty($deleted_content)) { if ($role === 'editor' || $role === 'admin') { unlink($deleted_content); + delete_comments($deleted_content); rebuilt_cache('all'); if ($destination == 'post') { $redirect = site_url(); @@ -1977,4 +1995,137 @@ function add_search_index($id, $content) save_json_pretty($filename, $search); } } -} \ No newline at end of file +} + +function rename_comments($oldfile, $newfile) { + $oldfile_comments = get_comments_file_from_md($oldfile); + $newfile_comments = get_comments_file_from_md($newfile); + + if (is_file($oldfile_comments) && $oldfile_comments != $newfile_comments) { + if (!is_dir(dirname($newfile_comments))) { + mkdir(dirname($newfile_comments), 0755, true); // true = recursively + } + return rename($oldfile_comments, $newfile_comments); + } + return true; +} + + +function delete_comments($mdfile) { + $file_comments = get_comments_file_from_md($mdfile); + if (is_file($file_comments)) { + unlink($file_comments); + return true; + } + return true; +} + +// Get URL from markdown file path +// Supports: blog posts, static pages, static subpages, and author profiles +// regardless if file is content .md file or comments .json file +function get_url_from_file($file) +{ + + // Normalize path separators (Windows/Linux)) + $file = str_replace('\\', '/', $file); + + if (preg_match('#^content/comments/#', $file)) { + $filetype = 'comments'; + $post_filename_parts = 2; + $file = preg_replace('#^content/comments/#', '', $file); + } elseif (preg_match('#^content/#', $file)) { + $filetype = 'content'; + $post_filename_parts = 3; + $file = preg_replace('#^content/#', '', $file); + } else { + $filetype = 'none'; + return null; + } + + // Split path into parts + $parts = explode('/', $file); + $basename = basename($file); + + // Check if it's an author profile: {username}/author.md + if (count($parts) == 2 && ($basename == 'author.md' || $basename == 'author.json')) { + $username = $parts[0]; + return 'author/' . $username; + // return site_url() . 'author/' . $username; + } + + // Check if it's a static page: static/{page}.md + if (count($parts) >= 2 && $parts[0] == 'static' && $basename != 'author.md' && $basename != 'author.json') { + $filename = pathinfo($basename, PATHINFO_FILENAME); + + // Check if it's a subpage: static/{parent}/[draft/]{subpage}.md + if (count($parts) >= 3) { + // Handle draft subpages: static/{parent}/draft/{subpage}.md + if ($parts[count($parts) - 2] == 'draft') { + // This is a draft subpage, extract parent and subpage slug + $parent = pathinfo($parts[count($parts) - 3], PATHINFO_FILENAME); + + // Remove number prefix if present (e.g., "01.about" -> "about") + $parent_parts = explode('.', $parent); + $parent_slug = isset($parent_parts[1]) ? $parent_parts[1] : $parent; + + $subpage_parts = explode('.', $filename); + $subpage_slug = isset($subpage_parts[1]) ? $subpage_parts[1] : $filename; + + return $parent_slug . '/' . $subpage_slug; + // return site_url() . $parent_slug . '/' . $subpage_slug; + } else { + // Regular subpage: static/{parent}/{subpage}.md + $parent = pathinfo($parts[1], PATHINFO_FILENAME); + + // Remove number prefix if present + $parent_parts = explode('.', $parent); + $parent_slug = isset($parent_parts[1]) ? $parent_parts[1] : $parent; + + $subpage_parts = explode('.', $filename); + $subpage_slug = isset($subpage_parts[1]) ? $subpage_parts[1] : $filename; + return $parent_slug . '/' . $subpage_slug; + // return site_url() . $parent_slug . '/' . $subpage_slug; + } + } + + // It's a regular static page + // Remove number prefix if present (e.g., "01.about" -> "about") + $page_parts = explode('.', $filename); + $slug = isset($page_parts[1]) ? $page_parts[1] : $filename; + return $slug; + // return site_url() . $slug; + } + + + + + // Check if it's a blog post: {username}/blog/{category}/{type}/[scheduled/]{date}_{tags}_{slug}.md + if (count($parts) >= 5 && $parts[1] == 'blog') { + $filename_parts = explode('_', pathinfo($basename, PATHINFO_FILENAME)); + + // Blog post filename format: {date}_{tags}_{slug} - + if (count($filename_parts) >= $post_filename_parts) { + $post_date = reset($filename_parts); + $post_slug = end($filename_parts); + + // Parse date from filename (format: Y-m-d-H-i-s) + $date_parts = explode('-', $post_date); + if (count($date_parts) >= 2) { + $year = $date_parts[0]; + $month = $date_parts[1]; + + // Check permalink type + if (permalink_type() == 'default') { + return $year . '/' . $month . '/' . $post_slug; + // return site_url() . $year . '/' . $month . '/' . $post_slug; + } else { + return permalink_type() . '/' . $post_slug; + // return site_url() . permalink_type() . '/' . $post_slug; + } + } + } + } + + // If none of the above patterns match, return null + return null; +} diff --git a/system/admin/views/comments.html.php b/system/admin/views/comments.html.php index 9f5ed4a..7fba747 100644 --- a/system/admin/views/comments.html.php +++ b/system/admin/views/comments.html.php @@ -61,8 +61,8 @@ - - + + @@ -81,17 +81,17 @@ + href="admin/comments/edit//"> @@ -101,8 +101,29 @@ +

.

+ + + +
+
+ +
+ @@ -260,9 +281,12 @@


-
+ + + +
0): ?> - +

@@ -177,7 +177,7 @@ if (isset($author[0])) {

0): ?> - +

diff --git a/system/htmly.php b/system/htmly.php index 92f56bd..78d4b94 100644 --- a/system/htmly.php +++ b/system/htmly.php @@ -2982,6 +2982,7 @@ post('/admin/users/:username/delete', function () { if ($role === 'admin') { if ($user_role !== 'admin') { unlink($file); + delete_comments($file); } } $redir = site_url() . 'admin/users'; @@ -3105,7 +3106,14 @@ get('/admin/comments', function () { if (login() && ($role === 'admin' || $role === 'editor')) { config('views.root', 'system/admin/views'); - $comments = getAllComments(); + $page = from($_GET, 'page'); + $page = $page ? (int)$page : 1; + $perpage = config('comments.perpage'); + + $result = getAllComments($page, $perpage); + $comments = $result[0]; + $total = $result[1]; + $pendingCount = getPendingCommentsCount(); render('comments', array( @@ -3118,8 +3126,10 @@ get('/admin/comments', function () { 'bodyclass' => 'admin-comments', 'breadcrumb' => '' . config('breadcrumb.home') . ' » ' . i18n('Comments'), 'tab' => 'all', + 'page' => $page, 'comments' => $comments, - 'pendingCount' => $pendingCount + 'pendingCount' => $pendingCount, + 'pagination' => has_pagination($total, $perpage, $page) )); } else { $login = site_url() . 'login'; @@ -3134,11 +3144,25 @@ get('/admin/comments/pending', function () { if (login() && ($role === 'admin' || $role === 'editor')) { config('views.root', 'system/admin/views'); + $page = from($_GET, 'page'); + $page = $page ? (int)$page : 1; + $perpage = 20; + + // Get all comments first to filter pending ones $allComments = getAllComments(); - $comments = array_filter($allComments, function($comment) { + $pendingComments = array_filter($allComments, function($comment) { return !$comment['published']; }); - $pendingCount = count($comments); + + // Reindex array after filtering + $pendingComments = array_values($pendingComments); + $total = count($pendingComments); + + // Paginate the pending comments + $offset = ($page - 1) * $perpage; + $comments = array_slice($pendingComments, $offset, $perpage); + + $pendingCount = $total; render('comments', array( 'title' => generate_title('is_default', i18n('Pending_Comments')), @@ -3150,8 +3174,10 @@ get('/admin/comments/pending', function () { 'bodyclass' => 'admin-comments', 'breadcrumb' => '' . config('breadcrumb.home') . ' » ' . i18n('Comments') . ' » ' . i18n('Pending'), 'tab' => 'pending', + 'page' => $page, 'comments' => $comments, - 'pendingCount' => $pendingCount + 'pendingCount' => $pendingCount, + 'pagination' => has_pagination($total, $perpage, $page) )); } else { $login = site_url() . 'login'; @@ -3247,18 +3273,21 @@ post('/admin/comments/settings', function () { }); // Show edit comment form -get('/admin/comments/edit/:postid/:commentid', function ($postid, $commentid) { +get('/admin/comments/edit/:commentfile/:commentid', function ($commentfile, $commentid) { $user = $_SESSION[site_url()]['user']; $role = user('role', $user); if (login() && ($role === 'admin' || $role === 'editor')) { + config('views.root', 'system/admin/views'); - $comments = getComments($postid, true); + $file = base64_decode(strtr($commentfile, '-_', '+/')); + $comments = getComments('', $file, true); $editComment = null; foreach ($comments as $comment) { if ($comment['id'] === $commentid) { - $comment['post_id'] = $postid; + $comment['file'] = $file; + $comment['file_encoded'] = $commentfile; $editComment = $comment; break; } @@ -3290,12 +3319,13 @@ get('/admin/comments/edit/:postid/:commentid', function ($postid, $commentid) { }); // Update comment -post('/admin/comments/update/:postid/:commentid', function ($postid, $commentid) { +post('/admin/comments/update/:commentfile/:commentid', function ($commentfile, $commentid) { $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')); if (login() && $proper) { $user = $_SESSION[site_url()]['user']; $role = user('role', $user); if ($role === 'admin' || $role === 'editor') { + $file = base64_decode(strtr($commentfile, '-_', '+/')); $data = array( 'name' => from($_POST, 'name'), 'email' => from($_POST, 'email'), @@ -3303,11 +3333,11 @@ post('/admin/comments/update/:postid/:commentid', function ($postid, $commentid) 'published' => isset($_POST['published']) ); - if (commentModify($postid, $commentid, $data)) { + if (commentModify($file, $commentid, $data)) { $redir = site_url() . 'admin/comments'; header("location: $redir"); } else { - $redir = site_url() . 'admin/comments/edit/' . $postid . '/' . $commentid; + $redir = site_url() . 'admin/comments/edit/' . $commentfile . '/' . $commentid; header("location: $redir"); } } else { @@ -3321,12 +3351,13 @@ post('/admin/comments/update/:postid/:commentid', function ($postid, $commentid) }); // Publish comment -get('/admin/comments/publish/:postid/:commentid', function ($postid, $commentid) { +get('/admin/comments/publish/:commentfile/:commentid', function ($commentfile, $commentid) { if (login()) { $user = $_SESSION[site_url()]['user']; $role = user('role', $user); if ($role === 'admin' || $role === 'editor') { - commentPublish($postid, $commentid); + $file = base64_decode(strtr($commentfile, '-_', '+/')); + commentPublish($file, $commentid); } } $redir = site_url() . 'admin/comments'; @@ -3334,12 +3365,13 @@ get('/admin/comments/publish/:postid/:commentid', function ($postid, $commentid) }); // Delete comment -get('/admin/comments/delete/:postid/:commentid', function ($postid, $commentid) { +get('/admin/comments/delete/:commentfile/:commentid', function ($commentfile, $commentid) { if (login()) { $user = $_SESSION[site_url()]['user']; $role = user('role', $user); if ($role === 'admin' || $role === 'editor') { - commentDelete($postid, $commentid); + $file = base64_decode(strtr($commentfile, '-_', '+/')); + commentDelete($file, $commentid); } } $redir = site_url() . 'admin/comments'; @@ -6046,7 +6078,7 @@ post('/comments/submit', function () { return; } - $postId = from($_POST, 'post_id'); + $url = from($_POST, 'url'); // used to set redirect and in commentInsert to set the .json file name $name = from($_POST, 'name'); $email = from($_POST, 'email'); $comment = from($_POST, 'comment'); @@ -6054,6 +6086,9 @@ post('/comments/submit', function () { $notify = from($_POST, 'notify'); $website = from($_POST, 'website'); // honeypot field + // Note: $url was also set in json file single comment block, but then it is hard to manage if .md file changes name or path + // introduced instead function get_url_from_file that handle both .md (content) and .json (content/comments) + $data = array( 'name' => $name, 'email' => $email, @@ -6063,15 +6098,15 @@ post('/comments/submit', function () { 'website' => $website ); - $result = commentInsert($postId, $data); + $result = commentInsert($data, $url, null); // Kept separate for future use if ($result['success']) { // Redirect back to post with success anchor - $redir = site_url() . $postId . '#comment-status+' . $result['message']; + $redir = site_url() . $url . '#comment-status+' . $result['message']; } else { // Redirect back to post with error - $redir = site_url() . $postId . '#comment-status+' . $result['message'][0]; + $redir = site_url() . $url . '#comment-status+' . $result['message']; } header("location: $redir"); diff --git a/system/includes/comments-frontend.php b/system/includes/comments-frontend.php index f8f1695..ef66001 100644 --- a/system/includes/comments-frontend.php +++ b/system/includes/comments-frontend.php @@ -8,7 +8,7 @@ if (!defined('HTMLY')) die('HTMLy'); * @param string $parentId Parent comment ID for replies (optional) * @return void */ -function displayCommentsForm($postId, $parentId = null) +function displayCommentsForm($url, $mdfile = null, $parentId = null) { if (!local()) { return; @@ -18,7 +18,7 @@ function displayCommentsForm($postId, $parentId = null) $submitUrl = site_url() . 'comments/submit'; ?> - + @@ -94,7 +94,7 @@ function displayComment($comment, $postId)
@@ -122,13 +122,13 @@ function displayComment($comment, $postId) * @param string $postId Post or page ID * @return void */ -function displayComments($postId) +function displayComments($url, $file = null) { if (!local()) { return; } - $comments = getComments($postId); + $comments = getComments($url, $file = null); if (empty($comments)) { return; @@ -142,7 +142,7 @@ function displayComments($postId) @@ -154,12 +154,20 @@ function displayComments($postId) * * @param string $postId Post or page ID * @return void + + * type can be post, author, page, subpage (same a view variable) + */ -function displayCommentsSection($postId) + + +function displayCommentsSection($url, $file = null) { if (!local()) { return; } + + $urlpath = ltrim(parse_url($url, PHP_URL_PATH), '/'); + ?>