2017-05-13 14:59:39 +00:00
< ? php
register_module ([
" name " => " Page Comments " ,
2017-05-20 11:33:26 +00:00
" version " => " 0.2 " ,
2017-05-13 14:59:39 +00:00
" 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 );
2017-05-20 13:50:17 +00:00
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->admindetails_name , $settings->sitename 's administrator, deleted the comment. Here's the comment you tried to post:</p>
2017-05-13 14:59:39 +00:00
< textarea readonly > $message </ textarea > " ));
}
$parent_comment -> replies [] = $new_comment ;
2017-05-20 13:50:17 +00:00
// Get an array of all the parent comments we need to notify
$comment_thread = fetch_comment_thread ( $comment_data , $parent_comment -> id );
2017-05-13 14:59:39 +00:00
$email_subject = " [Notification] $env->user replied to your comment on $env->page - $settings->sitename " ;
foreach ( $comment_thread as $thread_comment ) {
2017-05-16 19:42:15 +00:00
$email_body = " Hello, { username}! \n " .
2017-05-13 14:59:39 +00:00
" 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> " ));
}
2017-05-20 14:18:22 +00:00
// 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
]);
}
2017-05-13 14:59:39 +00:00
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 );
2017-05-13 15:37:06 +00:00
$comments_data = file_exists ( $comments_filename ) ? json_decode ( file_get_contents ( $comments_filename )) : [];
2017-05-13 14:59:39 +00:00
$comments_html = " <aside class='comments'> " .
2017-05-20 15:07:42 +00:00
" <h2 id='comments'>Comments</h2> \n " ;
2017-05-13 14:59:39 +00:00
if ( $env -> is_logged_in ) {
$comments_html .= " <form class='comment-reply-form' method='post' action='?action=comment&page= " . rawurlencode ( $env -> page ) . " '> \n " .
" <h3>Post a Comment</h3> \n " .
" \t <textarea name='message' placeholder='Type your comment here. You can use the same syntax you use when writing pages.'></textarea> \n " .
2017-05-13 16:03:25 +00:00
" \t <input type='hidden' name='replyto' /> \n " .
2017-05-13 14:59:39 +00:00
" \t <input type='submit' value='Post Comment' /> \n " .
" </form> \n " ;
2017-05-13 15:37:06 +00:00
}
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 " .
2017-05-14 20:51:04 +00:00
" \t <p class='not-logged-in'><a href='?action=login&returnto= " . rawurlencode ( " ?action=view&page= " . rawurlencode ( $env -> page )) . " '>Login</a> to post a comment.</p> \n " .
2017-05-13 16:03:25 +00:00
" \t <input type='hidden' name='replyto' /> \n " .
2017-05-14 20:51:04 +00:00
" \t <input disabled type='submit' value='Post Comment' title='Login to post a comment.' /> \n " .
2017-05-13 15:37:06 +00:00
" </form> \n " ;
}
$comments_html .= render_comments ( $comments_data );
$comments_html .= " </aside> \n " ;
2017-05-22 19:01:25 +00:00
$to_comments_link = " <div class='jump-to-comments'><a href='#comments'>Jump to comments</a></div> " ;
2017-05-20 15:07:42 +00:00
2017-05-13 15:37:06 +00:00
$parts [ " { extra} " ] = $comments_html . $parts [ " { extra} " ];
2017-05-20 15:07:42 +00:00
$parts [ " { content} " ] = str_replace_once ( " </h1> " , " </h1> \n $to_comments_link " , $parts [ " { content} " ]);
2017-05-13 15:37:06 +00:00
});
$reply_js_snippet = <<< 'REPLYJS'
2017-05-13 14:59:39 +00:00
///////////////////////////////////
///////// Commenting Form /////////
///////////////////////////////////
2017-05-13 15:37:06 +00:00
window . addEventListener ( " load " , function ( event ) {
2017-05-13 14:59:39 +00:00
var replyButtons = document . querySelectorAll ( " .reply-button " );
for ( let i = 0 ; i < replyButtons . length ; i ++ ) {
2017-05-13 15:37:06 +00:00
replyButtons [ i ] . addEventListener ( " click " , display_reply_form );
replyButtons [ i ] . addEventListener ( " touchend " , display_reply_form );
2017-05-13 14:59:39 +00:00
}
});
function display_reply_form ( event )
{
// Deep-clone the comment form
var replyForm = document . querySelector ( " .comment-reply-form " ) . cloneNode ( true );
2017-05-13 15:37:06 +00:00
replyForm . classList . add ( " nested " );
2017-05-13 14:59:39 +00:00
// Set the comment we're replying to
2017-05-14 20:51:04 +00:00
replyForm . querySelector ( " [name=replyto] " ) . value = event . target . parentElement . parentElement . parentElement . dataset . commentId ;
2017-05-13 14:59:39 +00:00
// Display the newly-cloned commenting form
2017-05-14 20:51:04 +00:00
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 );
2017-05-13 14:59:39 +00:00
}
2017-05-13 15:37:06 +00:00
2017-05-13 14:59:39 +00:00
REPLYJS ;
2017-05-13 15:37:06 +00:00
page_renderer :: AddJSSnippet ( $reply_js_snippet );
2017-05-13 14:59:39 +00:00
}
}
]);
/**
* 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 ));
2017-05-13 15:37:06 +00:00
$result = str_replace ([ " + " , " / " , " = " ], [ " - " , " _ " ], $result );
2017-05-13 14:59:39 +00:00
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 ) {
2017-05-20 13:50:17 +00:00
$subtrees [] = $comment -> replies ;
2017-05-13 14:59:39 +00:00
}
}
foreach ( $subtrees as $subtree )
{
2017-05-20 13:50:17 +00:00
$subtree_result = find_comment ( $subtree , $comment_id );
2017-05-13 14:59:39 +00:00
if ( $subtree_result !== false )
2017-05-20 13:50:17 +00:00
return $subtree_result ;
2017-05-13 14:59:39 +00:00
}
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 )
{
2017-05-20 11:33:26 +00:00
global $settings ;
2017-05-13 14:59:39 +00:00
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 ' data-comment-id=' $comment->id '> \n " ;
2017-05-14 20:51:04 +00:00
$result .= " \t <p class='comment-header'><span class='name'> $comment->username </span> said:</p> " ;
2017-05-13 14:59:39 +00:00
$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'> " ;
2017-05-14 20:51:04 +00:00
$result .= " \t \t <span class='comment-footer-item'><button class='reply-button'>Reply</button></span> \n " ;
$result .= " \t \t <span class='comment-footer-item'><a class='permalink-button' href='#comment- $comment->id ' title='Permalink to this comment'>🔗</a></span> \n " ;
2017-05-20 11:33:26 +00:00
$result .= " \t \t <span class='comment-footer-item'><time datetime=' " . date ( " c " , strtotime ( $comment -> timestamp )) . " ' title='The time this comment was posted'> $settings->comment_time_icon " . date ( " l jS \ of F Y \ a \\ t h:ia T " , strtotime ( $comment -> timestamp )) . " </time></span> \n " ;
2017-05-13 14:59:39 +00:00
$result .= " \t </p> \n " ;
2017-05-13 16:03:25 +00:00
$result .= " \t " . render_comments ( $comment -> replies , $depth + 1 ) . " \n " ;
2017-05-13 15:37:06 +00:00
$result .= " \t </div> " ;
2017-05-13 14:59:39 +00:00
}
$result .= " </div> " ;
return $result ;
}
?>