diff --git a/assets/comments.js b/assets/comments.js index 3d63de1..c366189 100644 --- a/assets/comments.js +++ b/assets/comments.js @@ -2,9 +2,9 @@ function escapeRegExp(str) { return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); } jQuery(document).ready(function () { - var commentForm = $(document).find('.comments-form'); - var commentSection = $(document).find('.comments').first(); - var commentAlert = $(document).find('.alert').first(); + var commentForm = $('#comments-form'); //$(document).find('.comments-form').first(); + var commentSection = $('#comments-section'); //$(document).find('.comments').first(); + var commentAlert = $('#comments-alert'); //$(document).find('.alert').first(); //hide form, show link commentForm.hide(); @@ -13,20 +13,78 @@ jQuery(document).ready(function () { //show comment form above comments section (new comment thread) $('body').on('click', '.comment-add-new', function (e) { e.preventDefault(); - //commentForm.hide(1000); + if ($(this).prev().filter('#comments-form').length > 0) { + //form is already in the right place. + //just make sure it is visible. + commentForm.show(); + return; + } + commentForm.hide(); //hide it to make sure that it is not shown after move to make "show" transition work. $(this).before(commentForm); commentForm.show('slow'); - commentAlert.slideUp(); + commentAlert.empty().slideUp(); }); //show comment form below selected comment (reply to existing comment) $('body').on('click', '.comment-add-reply', function (e) { e.preventDefault(); - var media = $(this).closest('.comment'); + var comment = $(this).closest('.comment'); + if (comment.find('#comments-form').length > 0) { + //form is already in the right place. + //just make sure it is visible. + commentForm.show(); + return; + } commentForm.hide(); - media.find('>.comment-body>.comment-text').after(commentForm); + comment.find('.comment-body').last().append(commentForm); commentForm.show('slow'); - commentAlert.slideUp(); + commentAlert.empty().slideUp(); + }); + + //delete comment (authorized user only) + $('body').on('click', '.comment-delete', function (e) { + e.preventDefault(); + var comment = $(this).closest('.comment'); + var id = parseInt(comment.attr('data-id'), 10); + var level = parseInt(comment.attr('data-level'), 10); + var nonce = commentForm.find("input[name='form-nonce']").val(); + if (comment.next().filter(".comment[data-level='" + (level + 1) + "']").length > 0) { + alert('Deletion not allowed. There are replies to this comment. Please delete them first.'); + return; + } + var url = commentForm.attr( "action" ); + var posting = $.post(url, { action: 'delete', id: id, nonce: nonce}, null, 'json'); + // Register events to ajax call + posting.done(function (response) { + + //make sure that commentForm is definitely not within the deleted DOM part. + //hide + //temporary move it outside the comment selected for deletion. (this definitely exists, not taking any chances here) + //finally move back to start of commentSection. (preferred target) + //Hint: Don't forget commentAlert as it is not inside the form. + commentAlert.empty().hide(); + commentForm.hide(); + comment.before(commentForm); + comment.before(commentAlert); + commentSection.prepend(commentAlert); + commentSection.prepend(commentForm); + //remove the comment and all content from DOM. + //detach would be a soft delete but as there is no reason to reuse the deleted comment, means should not be provided. + comment.remove(); + }); + posting.fail(function (status, error, title) { +//alert('error'); +//console.log("Response Data (fail)", JSON.parse(JSON.stringify(status))); + commentForm.after(commentAlert); + commentAlert.show(); + commentAlert.empty().append("

Error:

"); + commentAlert.append("

" + JSON.stringify(status) + "

"); + commentAlert.append("

" + JSON.stringify(error) + "

"); + commentAlert.append("

" + JSON.stringify(title) + "

"); + }); + posting.always(function () { + //alert("finished, be it successful or not"); + }); }); // Attach a submit handler to the form @@ -65,14 +123,12 @@ jQuery(document).ready(function () { commentAlert.css('color', 'green').empty().append(document.createTextNode( response.message )).fadeIn(30); var newMedia = "
" + "
" + - "" + - "user icon" + - "" + + "user icon" + "
" + "
" + "
" + "

{{comment.title}}

" + - "" + + "" + "
{{'PLUGIN_COMMENTS.WRITTEN_ON'|t}} {{comment.date|e}} {{'PLUGIN_COMMENTS.BY'|t}} {{comment.author}}
" + "
" + "
" + @@ -92,6 +148,7 @@ jQuery(document).ready(function () { newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.date|e}}"), 'g'), response.data.date); newMedia = newMedia.replace(new RegExp(escapeRegExp("{{nested}}"), 'g'), ''); newMedia = newMedia.replace(new RegExp(escapeRegExp("{{'PLUGIN_COMMENTS.ADD_REPLY'|t}}"), 'g'), response.data.ADD_REPLY); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{'PLUGIN_COMMENTS.REPLY'|t}}"), 'g'), response.data.REPLY); newMedia = newMedia.replace(new RegExp(escapeRegExp("{{'PLUGIN_COMMENTS.WRITTEN_ON'|t}}"), 'g'), response.data.WRITTEN_ON); newMedia = newMedia.replace(new RegExp(escapeRegExp("{{'PLUGIN_COMMENTS.BY'|t}}"), 'g'), response.data.BY); if ($( "div[data-id='" + response.data.parent_id + "']" ).length > 0) { @@ -101,7 +158,7 @@ jQuery(document).ready(function () { } } setTimeout(function () { - commentForm.hide(2000); + commentForm.slideUp(); commentAlert.fadeOut(5000); }, 5000); }); @@ -109,10 +166,11 @@ jQuery(document).ready(function () { //alert('error'); //console.log("Response Data (fail)", JSON.parse(JSON.stringify(status))); commentForm.after(commentAlert); - commentAlert.empty().append("

TEST

"); - commentAlert.append("

" + status + "

"); - commentAlert.append("

" + error + "

"); - commentAlert.append("

" + title + "

"); + commentAlert.show(); + commentAlert.empty().append("

Error:

"); + commentAlert.append("

" + JSON.stringify(status) + "

"); + commentAlert.append("

" + JSON.stringify(error) + "

"); + commentAlert.append("

" + JSON.stringify(title) + "

"); }); posting.always(function () { //alert("finished, be it successful or not"); diff --git a/comments.php b/comments.php index ecfbe11..5e9a67e 100644 --- a/comments.php +++ b/comments.php @@ -180,22 +180,71 @@ class CommentsPlugin extends Plugin // $this->only_full_stars = $this->config->get('plugins.star-ratings.only_full_stars'); $callback = $this->config->get('plugins.comments.ajax_callback'); // Process comment if required - if ($is_ajax || $callback === $this->grav['uri']->path()) { - // try to add the comment - $result = $this->addComment(true); - echo json_encode([ - 'status' => $result[0], - 'message' => $result[1], - 'data' => $result[2], -// 'data' => [ -// 'score' => $result[2][0], -// 'count' => $result[2][1] -// ] - ]); + if ($is_ajax) {// || $callback === $this->grav['uri']->path() + $action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING); + switch ($action) { + case 'addComment': + case '': + case null: + // try to add the comment + $result = $this->addComment(true); + echo json_encode([ + 'status' => $result[0], + 'message' => $result[1], + 'data' => $result[2], + ]); + break; + case 'delete': + // try to delete the comment + $result = $this->deleteComment(true); + echo json_encode([ + 'status' => $result[0], + 'message' => $result[1], + 'data' => $result[2], + ]); + break; + default: + //request unknown, present error page + //Set a 400 (bad request) response code. + http_response_code(400); + echo 'request malformed - action unknown'; + break; + } exit(); //prevents the page frontend from beeing displayed. } } + public function deleteComment() + { + $language = $this->grav['language']; + if (!$this->grav['user']->authorize('admin.super')) { + http_response_code(403); + return [false, 'access forbidden', [0, 0]]; + } + $id = filter_input(INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT); + $nonce = filter_input(INPUT_POST, 'nonce', FILTER_SANITIZE_STRING); + // ensure both values are sent + if (is_null($id) || is_null($nonce)) { + // Set a 400 (bad request) response code and exit. + http_response_code(400); + return [false, 'request malformed - missing parameter(s)', [0, 0]]; + } + if (!Utils::verifyNonce($nonce, 'comments')) { + http_response_code(403); + return [false, 'Invalid security nonce', [0, $nonce]]; + } + $lang = $this->grav['language']->getLanguage(); + $path = $this->grav['page']->path(); + $route = $this->grav['page']->route(); + $data = $this->removeComment($route, $path, $id, $lang); + if ($data[0]) { + return [true, $language->translate('PLUGIN_COMMENTS.DELETE_SUCCESS'), $data[1]]; + } else { + http_response_code(403); //forbidden + return [false, $language->translate('PLUGIN_COMMENTS.DELETE_FAIL'), $data[1]]; + } + } + public function addComment($is_ajax = false) { if($is_ajax) { @@ -254,6 +303,7 @@ class CommentsPlugin extends Plugin 'level' => 0, 'hash' => md5(strtolower(trim($comment['email']))), 'ADD_REPLY' => $language->translate('PLUGIN_COMMENTS.ADD_REPLY'), + 'REPLY' => $language->translate('PLUGIN_COMMENTS.REPLY'), 'WRITTEN_ON' => $language->translate('PLUGIN_COMMENTS.WRITTEN_ON'), 'BY' => $language->translate('PLUGIN_COMMENTS.BY'), ); @@ -265,6 +315,123 @@ class CommentsPlugin extends Plugin } } + /** + * Handle form processing instructions. + * + * @param Event $event + */ + public function removeComment($route, $path, $id, $lang) + { + $entry_removed = false; + $message = ''; + $date = time();//date('D, d M Y H:i:s', time()); + + /******************************/ + /** store comments with page **/ + /******************************/ + $localfilename = $path . '/comments.yaml'; + $localfile = CompiledYamlFile::instance($localfilename); + if (file_exists($localfilename)) { + $data = $localfile->content(); + if(isset($data['comments']) && is_array($data['comments'])) { + foreach($data['comments'] as $key => $comment) { + if(!empty($comment['parent_id']) && $comment['parent_id'] == $id) { + //hit an existing comment that is a reply to comment selected for deletion. + //deletion of "parent" comment not allowed to preserve integrity of nested comments. + //TODO: Alternatively allow it to mark parent comments as deleted + // and make sure (via Comment class / setCommentLevels) that children are + // filtered out from fetch regardless of their own deletion state. + $data['comments'][$key] = array_merge(array('deleted' => ''), $comment); + //set date after merge + //reason: could be possible that "deleted" already exists (e.g. false or '') in $comment which would overwrite the first (newly added) occurence + $data['comments'][$key]['deleted'] = $date; + //no need to look further as ids are supposed to be unique. + $localfile->save($data); + $entry_removed = false; + $reply_id = empty($comment['id']) ? '' : $comment['id']; + $message = "Found active reply ($reply_id) for selected comment ($id)."; + return [$entry_removed, $message]; + break; + } + } + foreach($data['comments'] as $key => $comment) { + if(!empty($comment['id']) && $comment['id'] == $id) { + //add deleted as first item in array (better readability in file) + $data['comments'][$key] = array_merge(array('deleted' => ''), $comment); + //set date after merge + //reason: could be possible that "deleted" already exists (e.g. false or '') in $comment which would overwrite the first (newly added) occurence + $data['comments'][$key]['deleted'] = $date; + //no need to look further as ids are supposed to be unique. + $localfile->save($data); + $entry_removed = true; + $message = "Deleted comment ($id) via path ($path)"; + break; + } + } + } + } else { + //nothing + } + /**********************************/ + /** store comments in index file **/ + /**********************************/ + $indexfilename = DATA_DIR . 'comments/index.yaml'; + $indexfile = CompiledYamlFile::instance($indexfilename); + if (file_exists($indexfilename)) { + $dataIndex = $indexfile->content(); + if(isset($dataIndex['comments']) && is_array($dataIndex['comments'])) { + foreach($dataIndex['comments'] as $key => $comment) { + if(!empty($comment['page']) && !empty($comment['id']) && $comment['page'] == $route && $comment['id'] == $id) { + //add deleted as first item in array (better readability in file) + $dataIndex['comments'][$key] = array_merge(array('deleted' => ''), $comment); + //set date after merge + //reason: could be possible that "deleted" already exists (e.g. false or '') in $comment which would overwrite the first (newly added) occurence + $dataIndex['comments'][$key]['deleted'] = $date; + //no need to look further as ids are supposed to be unique. + $indexfile->save($dataIndex); + break; + } + } + } + } else { + //nothing + } + /**************************************/ + /** store comments in old data files **/ + /** TODO: remove as soon as admin **/ + /** panel uses new index file **/ + /**************************************/ + $filename = DATA_DIR . 'comments'; + $filename .= ($lang ? '/' . $lang : ''); + $filename .= $path . '.yaml'; + $file = CompiledYamlFile::instance($filename); + + if (file_exists($filename)) { + $dataLegacy = $file->content(); + if(isset($dataLegacy['comments']) && is_array($dataLegacy['comments'])) { + foreach($dataLegacy['comments'] as $key => $comment) { + if(!empty($comment['id']) && $comment['id'] == $id) { + //add deleted as first item in array (better readability in file) + $dataLegacy['comments'][$key] = array_merge(array('deleted' => ''), $comment); + //set date after merge + //reason: could be possible that "deleted" already exists (e.g. false or '') in $comment which would overwrite the first (newly added) occurence + $dataLegacy['comments'][$key]['deleted'] = $date; + //no need to look further as ids are supposed to be unique. + $file->save($dataLegacy); + break; + } + } + } + } else { + //nothing + } + + //clear cache + $this->grav['cache']->delete($this->comments_cache_id); + + return [$entry_removed, $message]; + } + /** * Handle form processing instructions. * @@ -532,8 +699,14 @@ class CommentsPlugin extends Plugin } $levelsflat = array(); foreach($comments as $key => $comment) { - $levelsflat[$comment['id']]['parent'] = $comment['parent']; - $levelsflat[$comment['id']]['class'] = new Comment($comment['id'], $comments[$key]); + if(!empty($comment['deleted'])) { + //if field "deleted" exists and is filled with a true value then ignore the comment completely. + //TODO: This only works on this position as long as it is forbidden to delete comments that have active replies (children). + // Otherwise implement that children get the deleted flag recursively or are ignored via Comment class. + } else { + $levelsflat[$comment['id']]['parent'] = $comment['parent']; + $levelsflat[$comment['id']]['class'] = new Comment($comment['id'], $comments[$key]); + } } //get starting points (entries without valid parent = root element) $leveltree = array(); diff --git a/languages.yaml b/languages.yaml index 65b8db7..efc3d5a 100644 --- a/languages.yaml +++ b/languages.yaml @@ -1,10 +1,12 @@ de: PLUGIN_COMMENTS: ADD_NEW: Kommentar hinzufügen - ADD_REPLY: Antworten + ADD_REPLY: Auf Kommentar antworten ADD_COMMENT: Kommentar hinzufügen DELETE_COMMENT: Kommentar löschen - SUCCESS: Der Kommentar wurde erfolgreich gespeichert. + REPLY: Antworten + DELETE: Löschen + SUCCESS: "Der Kommentar wurde erfolgreich gespeichert." COMMENTS: Kommentare EMAIL_NOT_CONFIGURED: Email nicht konfiguriert NEW_COMMENT_EMAIL_SUBJECT: 'Neuer Kommentar für %1$s' @@ -27,10 +29,12 @@ de: en: PLUGIN_COMMENTS: ADD_NEW: Add a comment - ADD_REPLY: Reply + ADD_REPLY: Reply to comment ADD_COMMENT: Add a comment DELETE_COMMENT: Delete comment - SUCCESS: Comment has been saved successfully. + REPLY: Reply + DELETE: Delete + SUCCESS: "Comment has been saved successfully." COMMENTS: Comments EMAIL_NOT_CONFIGURED: Email not configured NEW_COMMENT_EMAIL_SUBJECT: 'New comment on %1$s' diff --git a/templates/partials/comments.form.html.twig b/templates/partials/comments.form.html.twig index 5be563e..be8d586 100644 --- a/templates/partials/comments.form.html.twig +++ b/templates/partials/comments.form.html.twig @@ -1,5 +1,4 @@ -

{{'PLUGIN_COMMENTS.ADD_COMMENT'|t}}

-
@@ -35,4 +34,4 @@ {{ nonce_field('comments', 'form-nonce')|raw }}
-
{{ form.message }}
+
{{ form.message }}
diff --git a/templates/partials/comments.html.twig b/templates/partials/comments.html.twig index e50ead3..9728aa0 100644 --- a/templates/partials/comments.html.twig +++ b/templates/partials/comments.html.twig @@ -1,27 +1,27 @@ {% if grav.twig.enable_comments_plugin %} {% set scope = scope ?: 'data.' %} +
+

{{'PLUGIN_COMMENTS.COMMENTS'|t}}

+ {% include 'partials/comments.form.html.twig' %} {% if grav.twig.comments|length %} -

{{'PLUGIN_COMMENTS.COMMENTS'|t}}

{{'PLUGIN_COMMENTS.ADD_NEW'|t}}
{% for comment in grav.twig.comments %}
- user icon -

{{comment.title}}

{{'PLUGIN_COMMENTS.WRITTEN_ON'|t}} {{comment.date|e}} {{'PLUGIN_COMMENTS.BY'|t}} {{comment.author}}
@@ -36,6 +36,7 @@
{% endif %} +
{% endif %}