"Page Comments", "version" => "0.2", "author" => "Starbeamrainbowlabs", "description" => "Adds threaded comments to the bottom of every page.", "id" => "feature-comments", "code" => function() { global $env; /** * @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 = "Jump to comments"; $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); } } ]); /** * 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 = "
$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