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.

This commit is contained in:
Emidio Reggiani 2025-11-25 18:41:15 +01:00
commit 6d3e33ceb7
8 changed files with 456 additions and 84 deletions

View file

@ -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"

View file

@ -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);
}
}
}
}
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;
}

View file

@ -61,8 +61,8 @@
<?php endif; ?>
</td>
<td>
<a href="<?php echo site_url() . $comment['post_id']; ?>" target="_blank">
<?php echo _h($comment['post_id']); ?>
<a href="<?php echo site_url() . get_url_from_file($comment['file']); ?>" target="_blank">
<?php echo _h(get_url_from_file($comment['file'])); ?>
</a>
</td>
<td>
@ -81,17 +81,17 @@
<td>
<?php if (!$comment['published']): ?>
<a class="btn btn-success btn-xs"
href="<?php echo site_url(); ?>admin/comments/publish/<?php echo $comment['post_id']; ?>/<?php echo $comment['id']; ?>"
href="<?php echo site_url(); ?>admin/comments/publish/<?php echo rtrim(strtr(base64_encode($comment['file']), '+/', '-_'), '='); ?>/<?php echo $comment['id']; ?>"
onclick="return confirm('<?php echo i18n('Confirm_publish_comment'); ?>');">
<?php echo i18n('Publish');?>
</a>
<?php endif; ?>
<a class="btn btn-primary btn-xs"
href="<?php echo site_url(); ?>admin/comments/edit/<?php echo $comment['post_id']; ?>/<?php echo $comment['id']; ?>">
href="<?php echo site_url(); ?>admin/comments/edit/<?php echo rtrim(strtr(base64_encode($comment['file']), '+/', '-_'), '='); ?>/<?php echo $comment['id']; ?>">
<?php echo i18n('Edit');?>
</a>
<a class="btn btn-danger btn-xs"
href="<?php echo site_url(); ?>admin/comments/delete/<?php echo $comment['post_id']; ?>/<?php echo $comment['id']; ?>"
href="<?php echo site_url(); ?>admin/comments/delete/<?php echo rtrim(strtr(base64_encode($comment['file']), '+/', '-_'), '='); ?>/<?php echo $comment['id']; ?>"
onclick="return confirm('<?php echo i18n('Confirm_delete_comment'); ?>');">
<?php echo i18n('Delete');?>
</a>
@ -101,8 +101,29 @@
</tbody>
</table>
<?php else: ?>
<?php if (!isset($editComment)): ?>
<p><?php echo i18n('No_comments_found'); ?>.</p>
<?php endif; ?>
<?php endif; ?>
<?php if (!empty($comments) && (!empty($pagination['prev']) || !empty($pagination['next']))): ?>
<br>
<div class="pager">
<ul class="pagination">
<?php if (!empty($pagination['prev'])) { ?>
<li class="newer page-item"><a class="page-link" href="?page=<?php echo $page - 1 ?>" rel="prev">&#8592; <?php echo i18n('Newer');?></a></li>
<?php } else { ?>
<li class="page-item disabled" ><span class="page-link">&#8592; <?php echo i18n('Newer');?></span></li>
<?php } ?>
<li class="page-number page-item disabled"><span class="page-link"><?php echo $pagination['pagenum'];?></span></li>
<?php if (!empty($pagination['next'])) { ?>
<li class="older page-item" ><a class="page-link" href="?page=<?php echo $page + 1 ?>" rel="next"><?php echo i18n('Older');?> &#8594;</a></li>
<?php } else { ?>
<li class="page-item disabled" ><span class="page-link"><?php echo i18n('Older');?> &#8594;</span></li>
<?php } ?>
</ul>
</div>
<?php endif; ?>
<?php elseif ($tab === 'settings'): ?>
<!-- Settings Form -->
@ -260,9 +281,12 @@
<!-- Edit Comment Modal/Page -->
<h3><?php echo i18n('Edit_Comment');?></h3>
<hr>
<form method="POST" action="<?php echo site_url(); ?>admin/comments/update/<?php echo $editComment['post_id']; ?>/<?php echo $editComment['id']; ?>">
<form method="POST" action="<?php echo site_url(); ?>admin/comments/update/<?php echo $editComment['file_encoded']; ?>/<?php echo $editComment['id']; ?>">
<input type="hidden" name="csrf_token" value="<?php echo get_csrf(); ?>">
<input type="hidden" name="url" value="<?php echo $editComment['url']; ?>">
<input type="hidden" name="file" value="<?php echo $editComment['file_encoded']; ?>">
<div class="form-group">
<label for="edit-name"><?php echo i18n('Name');?></label>
<input type="text" class="form-control" id="edit-name" name="name"

View file

@ -159,7 +159,7 @@ if (isset($author[0])) {
<?php
$pendingCount = getPendingCommentsCount();
if ($pendingCount > 0): ?>
<span class="badge badge-warning right"><?php echo $pendingCount; ?></span>
<span class="badge badge-warning right" style="margin-right: 15px;"><?php echo $pendingCount; ?></span><br>
<?php endif; ?>
<i class="right fa fa-angle-left"></i>
</p>
@ -177,7 +177,7 @@ if (isset($author[0])) {
<p>
<?php echo i18n('Pending_Moderation'); ?>
<?php if ($pendingCount > 0): ?>
<span class="badge badge-warning right"><?php echo $pendingCount; ?></span>
<span class="badge badge-warning right" style="margin-right: 15px;"><?php echo $pendingCount; ?></span>
<?php endif; ?>
</p>
</a>

View file

@ -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' => '<a href="' . site_url() . '">' . config('breadcrumb.home') . '</a> &#187; ' . 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' => '<a href="' . site_url() . '">' . config('breadcrumb.home') . '</a> &#187; ' . i18n('Comments') . ' &#187; ' . 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");

View file

@ -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';
?>
<form id="<?php echo $formId; ?>" method="POST" action="<?php echo $submitUrl; ?>" class="comment-form">
<input type="hidden" name="post_id" value="<?php echo _h($postId); ?>">
<input type="hidden" name="url" value="<?php echo _h($url); ?>">
<?php if ($parentId): ?>
<input type="hidden" name="parent_id" value="<?php echo _h($parentId); ?>">
<?php endif; ?>
@ -94,7 +94,7 @@ function displayComment($comment, $postId)
<?php echo formatCommentText($comment['comment']); ?>
</div>
<div class="comment-footer">
<button class="btn btn-sm btn-link reply-button" onclick="showReplyForm('<?php echo $comment['id']; ?>', '<?php echo $postId; ?>')">
<button class="btn btn-sm btn-link reply-button" onclick="showReplyForm('<?php echo $comment['id']; ?>', '<?php echo ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'); ?>')">
<i class="fa fa-reply"></i> <?php echo i18n('Reply'); ?>
</button>
</div>
@ -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)
<!--- <h4><?php echo i18n('Comments'); ?> (<?php echo count($comments); ?>)</h4> --->
<?php
foreach ($commentTree as $comment) {
displayComment($comment, $postId);
displayComment($comment, $url, $file = null);
}
?>
</div>
@ -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), '/');
?>
<section class="comments comment-box" id="comments">
<!---
@ -170,16 +178,16 @@ function displayCommentsSection($postId)
<div class="comment-alert-status" id="comment-alert-status" style="display:none;">
</div>
<?php displayComments($postId); ?>
<?php displayComments($urlpath, $file = null); ?>
<div class="comment-form-section">
<h4><?php echo i18n('Leave_a_comment'); ?></h4>
<?php displayCommentsForm($postId); ?>
<?php displayCommentsForm($urlpath, $file = null); ?>
</div>
</section>
<script>
function showReplyForm(commentId, postId) {
function showReplyForm(commentId, commentUrl) {
// Hide all other reply forms
document.querySelectorAll('.reply-container').forEach(function(el) {
el.style.display = 'none';
@ -196,7 +204,7 @@ function displayCommentsSection($postId)
var formId = 'reply-form-' + commentId;
var formHtml = '<form id="' + formId + '" method="POST" action="' + submitUrl + '" class="comment-form">' +
'<input type="hidden" name="post_id" value="' + postId + '">' +
'<input type="hidden" name="url" value="' + commentUrl + '">' +
'<input type="hidden" name="parent_id" value="' + commentId + '">' +
'<div style="position:absolute;left:-5000px;" aria-hidden="true">' +
'<input type="text" name="website" tabindex="-1" value="" autocomplete="off">' +

View file

@ -98,30 +98,124 @@ function save_comments_config($data = array())
/**
* Get comments file path for a post/page
* Replicates content file path inside comments folder
*
* @param string $postId Post or page ID
* @return string File path
* @param string $url is the page url (e.g., "post/htmly-simple-comment-system" or "author/emidio")
* @param string $file is the content markdown file (optional)
* @return string File path (e.g., "content/comments/post/htmly-simple-comment-system.json")
*/
function get_comments_file($postId)
function get_comments_file($url, $mdfile = null)
{
$dir = 'content/comments/';
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
// $view -> static/main/post/profile
if ($mdfile) {
if (preg_match('#comments[/\\\\].+\.json#', $mdfile)) {
if (is_file($mdfile)) {
return $mdfile;
}
return $dir . sanitize_filename($postId) . '.json';
}
else {
$comments_file = get_comments_file_from_md($mdfile);
}
}
else {
$comments_file = get_comments_file_from_url($url);
}
return $comments_file;
}
/**
* Sanitize filename
*
* @param string $filename
* @return string Sanitized filename
*/
function sanitize_filename($filename)
{
return preg_replace('/[^a-z0-9\-_]/i', '_', $filename);
function get_comments_file_from_md($mdfile) {
$file_parts = explode('_', pathinfo($mdfile, PATHINFO_FILENAME));
if (count($file_parts) > 1) {
$file = reset($file_parts) . '_' . end($file_parts) . '.json';
}
else {
$file = reset($file_parts) . '.json';
}
$path = preg_replace('/content/', 'content/comments', pathinfo($mdfile, PATHINFO_DIRNAME), 1);
return $path . '/' . $file;
}
function get_comments_file_from_url($url) {
// not using $view actually
// strip site_url prefix and query string if present
$url = trim(parse_url($url, PHP_URL_PATH), '/');
// 0. test pattern: author
if (preg_match('#^author/([^/]+)$#', $url, $matches)) {
$authorfile = 'content/' . $matches[1] . '/author.md';
$file = pathinfo($authorfile, PATHINFO_FILENAME) . '.json';
$path = preg_replace('/content/', 'content/comments', pathinfo($authorfile, PATHINFO_DIRNAME), 1);
return $path . '/' . $file;
}
// 1. test pattern: /YYYY/MM/name (post with default permalink)
if (preg_match('#^(\d{4})/(\d{2})/(.+)$#', $url, $matches)) {
$post = find_post($matches[1], $matches[2], $matches[3]);
if ($post && isset($post['current']->file)) {
$file_parts = explode('_', pathinfo($post['current']->file, PATHINFO_FILENAME));
$file = reset($file_parts) . '_' . end($file_parts) . '.json';
$path = preg_replace('/content/', 'content/comments', pathinfo($post['current']->file, PATHINFO_DIRNAME), 1);
return $path . '/' . $file; }
}
// 2. test pattern with custom permalink /[prefix]/name
$permalink_prefix = permalink_type();
if ($permalink_prefix != 'default' && strpos($url, $permalink_prefix . '/') === 0) {
$name = substr($url, strlen($permalink_prefix) + 1);
$post = find_post(null, null, $name);
if ($post && isset($post['current']->file)) {
$file_parts = explode('_', pathinfo($post['current']->file, PATHINFO_FILENAME));
$file = reset($file_parts) . '_' . end($file_parts) . '.json';
$path = preg_replace('/content/', 'content/comments', pathinfo($post['current']->file, PATHINFO_DIRNAME), 1);
return $path . '/' . $file;
}
}
// 4. test pattern static page: /slug
if (strpos($url, '/') === false) {
$page = find_page($url);
if ($page && isset($page['current']->file)) {
$file = pathinfo($page['current']->file, PATHINFO_FILENAME) . '.json';
$path = preg_replace('/content/', 'content/comments', pathinfo($page['current']->file, PATHINFO_DIRNAME), 1);
return $path . '/' . $file;
}
}
// 2. test pattern sub page: /parent/sub
if (substr_count($url, '/') == 1) {
list($parent, $sub) = explode('/', $url, 2);
$subpage = find_subpage($parent, $sub);
if ($subpage && isset($subpage['current']->file)) {
$file = pathinfo($subpage['current']->file, PATHINFO_FILENAME) . '.json';
$path = preg_replace('/content/', 'content/comments', pathinfo($subpage['current']->file, PATHINFO_DIRNAME), 1);
return $path . '/' . $file;
}
}
// not found
return null;
}
/**
* Get all comments for a post/page
*
@ -129,9 +223,9 @@ function sanitize_filename($filename)
* @param bool $includeUnpublished Include unpublished comments (for admin)
* @return array Comments array
*/
function getComments($postId, $includeUnpublished = false)
function getComments($url, $mdfile = null, $includeUnpublished = false)
{
$file = get_comments_file($postId);
$file = get_comments_file($url, $mdfile);
if (!file_exists($file)) {
return array();
@ -162,24 +256,38 @@ function getComments($postId, $includeUnpublished = false)
/**
* Get all comments across all posts (for admin)
*
* @return array All comments with post info
* @param int $page Page number (default: null for all comments)
* @param int $perpage Comments per page (default: null for all comments)
* @return array All comments with post info, or array with paginated comments and total count
*/
function getAllComments()
function getAllComments($page = null, $perpage = null)
{
$commentsDir = 'content/comments/';
if (!is_dir($commentsDir)) {
return array();
}
$files = glob($commentsDir . '*.json');
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($commentsDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
$files = array();
foreach ($iterator as $file) {
if ($file->isFile() && strtolower($file->getExtension()) === 'json') {
// $files[] = $file->getPathname();
$files[] = str_replace('\\', '/', $file->getPathname());
}
}
$allComments = array();
foreach ($files as $file) {
$postId = basename($file, '.json');
$comments = getComments($postId, true);
foreach ($files as $file) {
$comments = getComments('', $file, true);
foreach ($comments as $comment) {
$comment['post_id'] = $postId;
$comment['file'] = $file;
$allComments[] = $comment;
}
}
@ -189,6 +297,14 @@ function getAllComments()
return $b['timestamp'] - $a['timestamp'];
});
// If pagination parameters are provided, return paginated results
if ($page !== null && $perpage !== null) {
$total = count($allComments);
$offset = ($page - 1) * $perpage;
$paginatedComments = array_slice($allComments, $offset, $perpage);
return array($paginatedComments, $total);
}
return $allComments;
}
@ -272,7 +388,7 @@ function validateComment($data)
* @param array $data Comment data (name, email, comment, parent_id, notify)
* @return array Result with 'success' boolean and 'message' or 'comment_id'
*/
function commentInsert($postId, $data)
function commentInsert($data, $url, $mdfile = null)
{
// Validate comment
$validation = validateComment($data);
@ -284,7 +400,7 @@ function commentInsert($postId, $data)
}
// Get existing comments
$file = get_comments_file($postId);
$file = get_comments_file($url, $mdfile = null);
$comments = array();
if (file_exists($file)) {
$content = file_get_contents($file);
@ -316,6 +432,10 @@ function commentInsert($postId, $data)
// Save to file
$json = json_encode($comments, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// Create path if not exists
if (!is_dir(dirname($file))) {
mkdir(dirname($file), 0755, true); // true = recursively
}
if (file_put_contents($file, $json, LOCK_EX) === false) {
return array(
'success' => false,
@ -324,7 +444,7 @@ function commentInsert($postId, $data)
}
// Send notifications
sendCommentNotifications($postId, $comment, $comments);
sendCommentNotifications($url, $comment, $comments);
return array(
'success' => true,
@ -340,9 +460,9 @@ function commentInsert($postId, $data)
* @param string $commentId Comment ID
* @return bool Success status
*/
function commentPublish($postId, $commentId)
function commentPublish($file, $commentId)
{
$file = get_comments_file($postId);
if (!file_exists($file)) {
return false;
}
@ -361,7 +481,7 @@ function commentPublish($postId, $commentId)
$updated = true;
// Send notifications to other commenters
sendCommentNotifications($postId, $comment, $comments, false);
sendCommentNotifications($comment, $comments, false);
break;
}
}
@ -381,9 +501,9 @@ function commentPublish($postId, $commentId)
* @param string $commentId Comment ID
* @return bool Success status
*/
function commentDelete($postId, $commentId)
function commentDelete($url, $mdfile=null, $commentId)
{
$file = get_comments_file($postId);
$file = get_comments_file($url, $mdfile=null);
if (!file_exists($file)) {
return false;
}
@ -416,9 +536,8 @@ function commentDelete($postId, $commentId)
* @param array $data New comment data
* @return bool Success status
*/
function commentModify($postId, $commentId, $data)
function commentModify($file, $commentId, $data)
{
$file = get_comments_file($postId);
if (!file_exists($file)) {
return false;
}
@ -470,8 +589,10 @@ function commentModify($postId, $commentId, $data)
* @param bool $notifyAdmin Notify admin (default true)
* @return void
*/
function sendCommentNotifications($postId, $newComment, $allComments, $notifyAdmin = true)
function sendCommentNotifications($url, $newComment, $allComments, $notifyAdmin = true)
{
// TODO: function to be fixed, still using postId variable
// Check if notifications are enabled
if (comments_config('comments.notify') !== 'true' ||
comments_config('comments.mail.enabled') !== 'true') {
@ -602,9 +723,9 @@ function sendCommentEmail($to, $toName, $postId, $comment, $type = 'admin')
* @param bool $includeUnpublished Include unpublished comments
* @return int Comment count
*/
function getCommentCount($postId, $includeUnpublished = false)
function getCommentsCount($url, $mdfile = null, $includeUnpublished = false)
{
$comments = getComments($postId, $includeUnpublished);
$comments = getComments($url, $mdfile, $includeUnpublished);
return count($comments);
}

View file

@ -599,7 +599,7 @@ function route($method, $pattern, $callback = null)
if (!in_array($method, array('GET', 'POST')))
error(500, 'Only GET and POST are supported');
// a callback was passed, so we create a route defiition
// a callback was passed, so we create a route definition
if ($callback !== null) {
// create a route entry for this pattern
@ -611,7 +611,7 @@ function route($method, $pattern, $callback = null)
} else {
// callback is null, so this is a route invokation. look up the callback.
// callback is null, so this is a route invocation. look up the callback.
foreach ($route_map[$method] as $pat => $obj) {
// if the requested uri ($pat) has a matching route, let's invoke the cb