"Page Comments", "version" => "0.2.2", "author" => "Starbeamrainbowlabs", "description" => "Adds threaded comments to the bottom of every page.", "id" => "feature-comments", "code" => function() { global $env, $settings; /** * @api {post} ?action=comment Comment on a page * @apiName Comment * @apiGroup Comment * @apiPermission User * @apiDescription Posts a comment on a page, optionally in reply to another comment. Currently, comments must be made by a logged-in user. * * @apiParam {string} message The comment text. Supports the same syntax that the renderer of the main page supports. The default is extended markdown - see the help page of the specific wiki for more information. * @apiParam {string} replyto Optional. If specified the comment will be posted in reply to the comment with the specified id. * * * @apiError CommentNotFound The comment to reply to was not found. */ /* * ██████ ██████ ███ ███ ███ ███ ███████ ███ ██ ████████ * ██ ██ ██ ████ ████ ████ ████ ██ ████ ██ ██ * ██ ██ ██ ██ ████ ██ ██ ████ ██ █████ ██ ██ ██ ██ * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ██████ ██████ ██ ██ ██ ██ ███████ ██ ████ ██ */ add_action("comment", function() { global $settings, $env; $reply_to = $_POST["replyto"] ?? null; $message = $_POST["message"] ?? ""; if(!$env->is_logged_in) { http_response_code(401); exit(page_renderer::render_main("Error posting comment - $settings->sitename", "
Your comment couldn't be posted because you're not logged in. You can login here. Here's the comment you tried to post:
")); } $message_length = strlen($message); if($message_length < $settings->comment_min_length) { http_response_code(422); exit(page_renderer::render_main("Error posting comment - $settings->sitename", "Your comment couldn't be posted because it was too short. $settings->sitename needs at $settings->comment_min_length characters in a comment in order to post it.
")); } if($message_length > $settings->comment_max_length) { http_response_code(422); exit(page_renderer::renderer_main("Error posting comment - $settings->sitename", "Your comment couldn't be posted because it was too long. $settings->sitenamae can only post comments that are up to $settings->comment_max_length characters in length, and yours was $message_length characters. Try splitting it up into multiple comments! Here's the comment you tried to post:
")); } // Figure out where the comments are stored $comment_filename = get_comment_filename($env->page); if(!file_exists($comment_filename)) { if(file_put_contents($comment_filename, "[]\n") === false) { http_response_code(503); exit(page_renderer::renderer_main("Error posting comment - $settings->sitename", "$settings->sitename ran into a problem whilst creating a file to save your comment to! Please contact $settings->admindetails_name, $settings->sitename's administrator and tell them about this problem.
")); } } $comment_data = json_decode(file_get_contents($comment_filename)); $new_comment = new stdClass(); $new_comment->id = generate_comment_id(); $new_comment->timestamp = date("c"); $new_comment->username = $env->user; $new_comment->logged_in = $env->is_logged_in; $new_comment->message = $message; $new_comment->replies = []; if($reply_to == null) $comment_data[] = $new_comment; else { $parent_comment = find_comment($comment_data, $reply_to); if($parent_comment === false) { http_response_code(422); exit(page_renderer::render_main("Error posting comment - $settings->sitename", "$settings->sitename couldn't post your comment because it couldn't find the parent comment you replied to. It's possible that $settings->admindetails_name, $settings->sitename's administrator, deleted the comment. Here's the comment you tried to post:
")); } $parent_comment->replies[] = $new_comment; // Get an array of all the parent comments we need to notify $comment_thread = fetch_comment_thread($comment_data, $parent_comment->id); $email_subject = "[Notification] $env->user replied to your comment on $env->page - $settings->sitename"; foreach($comment_thread as $thread_comment) { $email_body = "Hello, {username}!\n" . "It's $settings->sitename here, letting you know that " . "someone replied to your comment (or a reply to your comment) on $env->page.\n" . "\n" . "They said:\n" . "\n" . "$new_comment->message" . "\n" . "You said on " . date("c", strtotime($thread_comment->timestamp)) . ":\n" . "\n" . "$thread_comment->message\n" . "\n"; email_user($thread_comment->username, $email_subject, $email_body); } } // Save the comments back to disk if(file_put_contents($comment_filename, json_encode($comment_data, JSON_PRETTY_PRINT)) === false) { http_response_code(503); exit(page_renderer::renderer_main("Error posting comment - $settings->sitename", "$settings->sitename ran into a problem whilst saving your comment to disk! Please contact $settings->admindetails_name, $settings->sitename's administrator and tell them about this problem.
")); } // Add a recent change if the recent changes module is installed if(module_exists("feature-recent-changes")) { add_recent_change([ "type" => "comment", "timestamp" => time(), "page" => $env->page, "user" => $env->user, "reply_depth" => count($comment_thread), "comment_id" => $new_comment->id ]); } http_response_code(307); header("location: ?action=view&page=" . rawurlencode($env->page) . "&commentsuccess=yes#comment-$new_comment->id"); exit(page_renderer::render_main("Comment posted successfully - $settings->sitename", "Your comment on $env->page was posted successfully. If your browser doesn't redirect you automagically, please click here to go to the comment you posted on the page you were viewing.
")); }); if($env->action == "view") { page_renderer::register_part_preprocessor(function(&$parts) { global $env; $comments_filename = get_comment_filename($env->page); $comments_data = file_exists($comments_filename) ? json_decode(file_get_contents($comments_filename)) : []; $comments_html = "\n"; $to_comments_link = ""; $parts["{extra}"] = $comments_html . $parts["{extra}"]; $parts["{content}"] = str_replace_once("", "\n$to_comments_link", $parts["{content}"]); }); $reply_js_snippet = <<<'REPLYJS' /////////////////////////////////// ///////// Commenting Form ///////// /////////////////////////////////// window.addEventListener("load", function(event) { var replyButtons = document.querySelectorAll(".reply-button"); for(let i = 0; i < replyButtons.length; i++) { replyButtons[i].addEventListener("click", display_reply_form); replyButtons[i].addEventListener("touchend", display_reply_form); } }); function display_reply_form(event) { // Deep-clone the comment form var replyForm = document.querySelector(".comment-reply-form").cloneNode(true); replyForm.classList.add("nested"); // Set the comment we're replying to replyForm.querySelector("[name=replyto]").value = event.target.parentElement.parentElement.parentElement.dataset.commentId; // Display the newly-cloned commenting form var replyBoxContiner = event.target.parentElement.parentElement.parentElement.querySelector(".reply-box-container"); replyBoxContiner.classList.add("active"); replyBoxContiner.appendChild(replyForm); // Hide the reply button so it can't be pressed more than once - that could // be awkward :P event.target.parentElement.removeChild(event.target); } REPLYJS; page_renderer::AddJSSnippet($reply_js_snippet); } add_help_section("29-commenting", "Commenting", "$settings->sitename has a threaded commenting system on every page. You can find it below each page's content, and can either leave a new comment, or reply to an existing one. If you reply to an existing one, then the authors of all the comments above yours will get notified by email of your reply - so long as they have an email address registered in their preferences.
"); } ]); /** * Given a page name, returns the absolute file path in which that page's * comments are stored. * @param string $pagename The name pf the page to fetch the comments filename for. * @return string The path to the file that the */ function get_comment_filename($pagename) { global $env; $pagename = makepathsafe($pagename); return "$env->storage_prefix$pagename.comments.json"; } /** * Generates a new random comment id. * @return string A new random comment id. */ function generate_comment_id() { $result = base64_encode(random_bytes(16)); $result = str_replace(["+", "/", "="], ["-", "_"], $result); return $result; } /** * Finds the comment with specified id by way of an almost-breadth-first search. * @param array $comment_data The comment data to search. * @param string $comment_id The id of the comment to find. * @return object The comment data with the specified id, or * false if it wasn't found. */ function find_comment($comment_data, $comment_id) { $subtrees = []; foreach($comment_data as $comment) { if($comment->id === $comment_id) return $comment; if(count($comment->replies) > 0) { $subtrees[] = $comment->replies; } } foreach($subtrees as $subtree) { $subtree_result = find_comment($subtree, $comment_id); if($subtree_result !== false) return $subtree_result; } return false; } /** * Fetches all the parent comments of the specified comment id, including the * comment itself at the end. * Useful for figuring out who needs notifying when a new comment is posted. * @param array $comment_data The comment data to search. * @param string $comment_id The comment id to fetch the thread for. * @return object[] A list of the comments in the thread, with the deepest * one at the end. */ function fetch_comment_thread($comment_data, $comment_id) { foreach($comment_data as $comment) { // If we're the comment they're looking for, then return ourselves as // the beginning of a thread if($comment->id === $comment_id) return [ $comment ]; if(count($comment->replies) > 0) { $subtree_result = fetch_comment_thread($comment->replies, $comment_id); if($subtree_result !== false) { // Prepend ourselves to the result array_unshift($subtree_result, $comment); return $subtree_result; // Return the comment thread } } } return false; } /** * Renders a given comments tree to html. * @param object[] $comments_data The comments tree to render. * @param integer $depth For internal use only. Specifies the depth * at which the comments are being rendered. * @return string The given comments tree as html. */ function render_comments($comments_data, $depth = 0) { global $settings; if(count($comments_data) == 0) { if($depth == 0) return "No comments here! Start the conversation above.
"; else return ""; } $result = "
" . page_renderer::render_username($comment->username) . " said:
"; $result .= "\t"; $result .= "\t\t\n"; $result .= "\t\t🔗\n"; $result .= "\t\t\n"; $result .= "\t
\n"; $result .= "\t" . render_comments($comment->replies, $depth + 1) . "\n"; $result .= "\t