Create commenting system for logged in users. It's currently untested!

This commit is contained in:
Starbeamrainbowlabs 2017-05-13 15:35:18 +01:00
parent 2407d6e542
commit 7c2a0a2e91
4 changed files with 349 additions and 2 deletions

View File

@ -131,6 +131,8 @@ $guiConfig = <<<'GUICONFIG'
"index.php?action=help"
]
]},
"comment_max_length": {"type": "number", "description": "The maximum allowed length, in characters, for comments", "default": 5000 },
"comment_min_length": {"type": "number", "description": "The minimum allowed length, in characters, for comments", "default": 10 },
"upload_enabled": {"type": "checkbox", "description": "Whether to allow uploads to the server.", "default": true},
"upload_allowed_file_types": {"type": "array", "description": "An array of mime types that are allowed to be uploaded.", "default": [
"image/jpeg",
@ -257,6 +259,7 @@ main:not(.printable) { padding: 2rem 2rem 0.5rem 2rem; background: #faf8fb; box-
blockquote { padding-left: 1em; border-left: 0.2em solid #442772; border-radius: 0.2rem; }
a { cursor: pointer;; }
a.redlink:link { color: rgb(230, 7, 7); }
a.redlink:visited { color: rgb(130, 15, 15); /*#8b1a1a*/ }
@ -622,6 +625,8 @@ function makepathsafe($string)
$string = preg_replace("/[?%*:|\"><()\\[\\]]/i", "", $string);
// Collapse multiple dots into a single dot
$string = preg_replace("/\.+/", ".", $string);
// Don't allow slashes at the beginning
$string = ltrim($string, "\\/");
return $string;
}
@ -937,6 +942,53 @@ function extract_user_from_userpage($userPagename) {
return $matches[1];
}
/**
* Sends a plain text email to a user, replacing {username} with the specified username.
* @param string $username The username to send the email to.
* @param string $subject The subject of the email.
* @param string $body The body of the email.
* @return boolean Whether the email was sent successfully or not. Currently, this may fail if the user doesn't have a registered email address.
*/
function email_user($username, $subject, $body)
{
global $version, $settings;
// If the user doesn't have an email address, then we can't email them :P
if(empty($settings->users->{$username}->emailAddress))
return false;
$subject = str_replace("{username}", $username, $subject);
$body = str_replace("{username}", $username, $body);
$headers = [
"content-type" => "text/plain",
"x-mailer" => "$settings->sitename Pepperminty-Wiki/$version PHP/" . phpversion(),
"reply-to" => "$settings->admindetails_name <$settings->admindetails_email>"
];
$compiled_headers = "";
foreach($headers as $header => $value)
$compiled_headers .= "$header: $value\r\n";
mail($settings->users->{$username->emailAddress}, $subject, $body, $compiled_headers, "-t");
return true;
}
/**
* Sends a plain text email to a list of users, replacing {username} with each user's name.
* @param string[] $usernames A list of usernames to email.
* @param string $subject The subject of the email.
* @param string $body The body of the email.
* @return integer The number of emails sent successfully.
*/
function email_users($usernames, $subject, $body)
{
$emailsSent = 0;
foreach($usernames as $username)
{
$emailsSent += email_user($username, $subject, $body) ? 1 : 0;
}
return $emailsSent;
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
@ -1201,7 +1253,7 @@ class page_renderer
<main>
{content}
</main>
{extra}
<footer>
<p>{footer-message}</p>
<p>Powered by Pepperminty Wiki v0.14-dev, which was built by <a href='//starbeamrainbowlabs.com/'>Starbeamrainbowlabs</a>. Send bugs to 'bugs at starbeamrainbowlabs dot com' or <a href='//github.com/sbrl/Pepperminty-Wiki' title='Github Issue Tracker'>open an issue</a>.</p>
@ -1292,6 +1344,7 @@ class page_renderer
/// Secondary Parts ///
"{content}" => $content,
"{extra}" => "",
"{title}" => $title,
];
@ -2001,6 +2054,288 @@ function render_sidebar($pageindex, $root_pagename = "")
register_module([
"name" => "Page Comments",
"version" => "0.1",
"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", "<p>Your comment couldn't be posted because you're not logged in. You can login <a href='?action=index'>here</a>. Here's the comment you tried to post:</p>
<textarea readonly>$message</textarea>"));
}
$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", "<p>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.</p>"));
}
if($message_length > $settings->comment_max_length) {
http_response_code(422);
exit(page_renderer::renderer_main("Error posting comment - $settings->sitename", "<p>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:</p>
<textarea readonly>$message</textarea>"));
}
// 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", "<p>$settings->sitename ran into a problem whilst creating a file to save your comment to! Please contact <a href='mailto:" . hide_email($settings->admindetails_email) . "'>$settings->admindetails_name</a>, $settings->sitename's administrator and tell them about this problem.</p>"));
}
}
$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", "<p>$settings->sitename couldn't post your comment because it couldn't find the parent comment you replied to. It's possible that $settings->adamindetails_name, $settings->sitename's administrator, deleted the comment. Here's the comment you tried to post:</p>
<textarea readonly>$message</textarea>"));
}
$parent_comment->replies[] = $new_comment;
$comment_thread = fetch_comment_thread($comment_data, $new_comment->id);
$email_subject = "[Notification] $env->user replied to your comment on $env->page - $settings->sitename";
foreach($comment_thread as $thread_comment) {
// Don't notify the comment poster of their own comment :P
if($thread_comment->id = $new_comment->id)
continue;
$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", "<p>$settings->sitename ran into a problem whilst saving your comment to disk! Please contact <a href='mailto:" . hide_email($settings->admindetails_email) . "'>$settings->admindetails_name</a>, $settings->sitename's administrator and tell them about this problem.</p>"));
}
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", "<p>Your comment on $env->page was posted successfully. If your browser doesn't redirect you automagically, please <a href='?action=view&page=" . rawurlencode($env->page) . "commentsuccess=yes#comment-$new_comment->id'>click here</a> to go to the comment you posted on the page you were viewing.</p>"));
});
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($comment_filename)) : [];
$comments_html = "<aside class='comments'>" .
"<h2>Comments</h2>\n" .
"<h3>Post a Comment</h3>\n";
if($env->is_logged_in) {
$comments_html .= "<form class='comment-reply-form' method='post' action='?action=comment&page=" . rawurlencode($env->page) . "'>\n" .
"\t<textarea name='message' placeholder='Type your comment here. You can use the same syntax you use when writing pages.'></textarea>\n" .
"\t<input type='hidden' name='reply-to' />\n" .
"\t<input type='submit' value='Post Comment' />\n" .
"</form>\n";
}
else {
$comments_html .= "<form class='comment-reply-form disabled no-login'>\n" .
"\t<textarea disabled name='message' placeholder='Type your comment here. You can use the same syntax you use when writing pages.'></textarea>\n" .
"\t<p><a href='?action=login&returnto=" . rawurlencode("?action=view&page=" . rawurlencode($env->page)) . "'>Login</a> to post a comment.</p>\n" .
"\t<input disabled type='submit' value='Post Comment' />\n" .
"</form>\n";
}
$comments_html .= render_comments($comments_data);
$comments_html .= "</aside>\n";
$parts["{extra}"] = $comments_html . $parts["{extra}"];
});
}
}
]);
/**
* 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);
if($subtree_result !== false)
return $subtree;
}
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)
{
if(count($comments_data) == 0) {
if($depth == 0)
return "<p><em>No comments here! Start the conversation above.</em></p>";
else
return "";
}
$result = "<div class='comments-list" . ($depth > 0 ? " nested" : "") . "' data-depth='$depth'>";
foreach($comments_data as $comment) {
$result .= "\t<div class='comment' id='comment-$comment->id'>\n";
$result .= "\t<p class='comment-header'>$comment->username said:</p>";
$result .= "\t<div class='comment-body'>\n";
$result .= "\t\t" . parse_page_source($comment->message);
$result .= "\t</div>\n";
$result .= "\t<div class='reply-box-container'></div>\n";
$result .= "\t<p class='comment-footer'>";
$result .= "\t\t<button class='reply-button'>Reply</button>\n";
$result .= "\t\t<a class='permalink-button' href='#comment-$comment->id'>&#x1f517;</a>\n";
$result .= "\t\t&#x1f557; <time datetime='" . date("c", $comment->timestamp) . "'>" . date("l jS \of F Y \a\\t h:ia T", $comment->timestamp) . "</time>\n";
$result .= "\t</p>\n";
$result .= "\t" . render_comments($comment->replies) . "\n";
$ersult .= "\t</div>";
}
$result .= "</div>";
return $result;
}
register_module([
"name" => "Settings GUI",
"version" => "0.1.1",

View File

@ -44,6 +44,15 @@
"lastupdate": 1450704211,
"optional": false
},
{
"name": "Page Comments",
"version": "0.1",
"author": "Starbeamrainbowlabs",
"description": "Adds threaded comments to the bottom of every page.",
"id": "feature-comments",
"lastupdate": 1494686069,
"optional": false
},
{
"name": "Settings GUI",
"version": "0.1.1",
@ -104,7 +113,7 @@
"author": "Starbeamrainbowlabs",
"description": "Adds a user preferences page, letting pople do things like change their email address and password.",
"id": "feature-user-preferences",
"lastupdate": 1490040319,
"lastupdate": 1492444821,
"optional": false
},
{

View File

@ -109,6 +109,8 @@
"index.php?action=help"
]
]},
"comment_max_length": {"type": "number", "description": "The maximum allowed length, in characters, for comments", "default": 5000 },
"comment_min_length": {"type": "number", "description": "The minimum allowed length, in characters, for comments", "default": 10 },
"upload_enabled": {"type": "checkbox", "description": "Whether to allow uploads to the server.", "default": true},
"upload_allowed_file_types": {"type": "array", "description": "An array of mime types that are allowed to be uploaded.", "default": [
"image/jpeg",

View File

@ -51,6 +51,7 @@ main:not(.printable) { padding: 2rem 2rem 0.5rem 2rem; background: #faf8fb; box-
blockquote { padding-left: 1em; border-left: 0.2em solid #442772; border-radius: 0.2rem; }
a { cursor: pointer;; }
a.redlink:link { color: rgb(230, 7, 7); }
a.redlink:visited { color: rgb(130, 15, 15); /*#8b1a1a*/ }