diff --git a/README.md b/README.md index 241882a..cf83fe8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Grav Comments Plugin \[Fork\] -This plugin adds support for displaying pingbacks, toggling the display of pingbacks. I'm not yet sure how pingbacks work, so this may not include support for adding new pingbacks; if it won't take too much work I'll add more support for them. Also added support for user entered URLs, though I haven't gotten the form input to show up yet. +This plugin adds support for displaying pingbacks, toggling the display of pingbacks. I'm not yet sure how pingbacks work, so this may not include support for adding new pingbacks though I attempted to patch the php file so it could work in theory; if it won't take too much work I'll add more support for them. Also added support for user entered URLs, though I haven't gotten the form input to show up yet. This fork adds support for Akismet, which is now basically working. Consider it alpha. Also has admin panel functionality, can toggle it, add api key, shows error on comment page if api key doesn't match site. Going to add amin panel checkbox for recaptcha. diff --git a/assets/comments.css b/assets/comments.css new file mode 100644 index 0000000..3341e31 --- /dev/null +++ b/assets/comments.css @@ -0,0 +1,86 @@ +/* +=============================================================================================================================== +Comments Plugin Styles +=============================================================================================================================== +*/ + +.comment { + margin-top: 0px; + width:100%; +} +.comment:first-child { + margin-top: 0px; +} +.comment, +.comment-body { + zoom: 1; +} +.comment-body { + overflow: hidden; + margin-left: 10px; +} +.comment-object { + display: block; +} +.comment-right, +.comment > .pull-right { + padding-left: 10px; +} +.comment-left, +.comment > .pull-left { + padding-right: 10px; +} +.comment-middle { + vertical-align: middle; +} +.comment-bottom { + vertical-align: bottom; +} +.comment-heading { + margin-top: 0px; + margin-bottom: 5px; +} +.comment-meta { + font-size: small; +} +.comment-text { + clear: both; +} +.comment-list { + padding-left: 0px; + list-style: none; +} +.comment-flag-new { + background-color: lightcyan; +} +.comment-wrapper { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; +} +.comment-avatar { + width: 20px; + height: 20px; + margin-left: -10px; + margin-right:5px; + vertical-align: middle; +} +/*.comment-thread-line:hover { + border-left:3px solid #1BB3E9; +}*/ +.comment-thread-top { + margin-top:20px; +} +.comment-thread { + float: left; + border-left:3px solid #444; + margin-left:10px; +} +.comment-footer { + font-size: small; +} +.comment-reply { + display: inline; +} \ No newline at end of file diff --git a/assets/comments.js b/assets/comments.js new file mode 100644 index 0000000..4602015 --- /dev/null +++ b/assets/comments.js @@ -0,0 +1,216 @@ +function escapeRegExp(str) { + return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); +} +jQuery(document).ready(function() { + 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(); + $(document).find('.comment-add-new').show(); + + //show comment form above comments section (new comment thread) + $('body').on('click', '.comment-add-new', function(e) { + e.preventDefault(); + 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.empty().slideUp(); + }); + + //show comment form below selected comment (reply to existing comment) + $('body').on('click', '.comment-add-reply', function(e) { + e.preventDefault(); + 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(); + comment.find('.comment-body').last().append(commentForm); + commentForm.show('slow'); + 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 + $(commentForm).on('submit', function(event) { + event.preventDefault(); + // Get form data: + var data = $(this).serialize(); + //console.log("Form Data (submit)", JSON.parse(JSON.stringify(data))); + var url = $(this).attr("action"); + //var url = '/nested-comments'; + var parent = 0; + var ownLevel = 0; + if ($(this).parents('.comment').length > 0) { + parent = $(this).closest('.comment').attr('data-id'); + ownLevel = parseInt($(this).closest('.comment').attr('data-level'), 10) + 1; + } + + // Send the data using post + //var posting = $.post(url, { parent: parent, data: data }, null, 'json'); + var posting = $.post(url, data + '&parent=' + parent, null, 'json'); + + // Register events to ajax call + posting.done(function(response) { + //alert('success'); + //console.log("Response Data (done)", JSON.parse(JSON.stringify(response))); + //response = JSON.parse(response); //not needed, post was done using json + commentForm.after(commentAlert); + if (!response.status) { + //should not trigger at all, if all bad requests return the right http status code + //i.e. <> 200 success => thus triggering posting.fail() + //leave this check just in case + commentAlert.stop().css('opacity', 1).text('Error: ' + response.message).fadeIn(30).fadeOut(5000); + return; + } + if (response.status) { + commentAlert.css('color', 'green').empty().append(document.createTextNode(response.message)).fadeIn(30); + /*var newMedia = "
" + + "
" + + "user icon" + + "
" + + "
" + + "
" + + "

{{comment.title}}

" + + "" + + "
{{'PLUGIN_COMMENTS.WRITTEN_ON'|t}} {{comment.date|e}} {{'PLUGIN_COMMENTS.BY'|t}} {{comment.author}}
" + + "
" + + "
" + + "{{comment.text}}" + + "
" + + "{{nested}}" + + "
" + + "
";*/ + var newMedia = `
+
+ user icon + + {% if comment.site %} + {{comment.author}} + {% else %} + {{comment.author}} + {% endif %} + + +
+
+
+ {{comment.text}} +
+ {{nested}} + +
+
`; + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.id}}"), 'g'), response.data.id); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.level|e}}"), 'g'), ownLevel); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.level}}"), 'g'), ownLevel); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.email|trim|lower|md5}}"), 'g'), response.data.hash); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{parent}}"), 'g'), response.data.parent); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.title}}"), 'g'), response.data.title); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.text}}"), 'g'), response.data.text); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.author}}"), 'g'), response.data.name); + newMedia = newMedia.replace(new RegExp(escapeRegExp("{{comment.site}}"), 'g'), response.data.site); + 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); + if ($("div[data-id='" + response.data.parent + "']").length > 0) { + $("div[data-id='" + response.data.parent + "']").first().after(newMedia); + } else { + $("#comments").prepend(newMedia); + } + } + setTimeout(function() { + commentForm.slideUp(); + commentAlert.fadeOut(5000); + }, 5000); + }); + 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"); + }); + }); +}); \ No newline at end of file diff --git a/class/Comment.php b/class/Comment.php new file mode 100644 index 0000000..5219f53 --- /dev/null +++ b/class/Comment.php @@ -0,0 +1,44 @@ +id = $id; + $this->value = $content; + } + + public function addItem($obj, $key = null) { + } + + public function deleteItem($key) { + } + + public function getItem($key) { + } + + public function getContent($level = 0) { + $this->value['level'] = $level; + $comments[] = $this->value; + + foreach($this->children as $child) { + //$comments[] = $child->getContent($level + 1); //produces nested result array. + $comments = array_merge($comments, $child->getContent($level + 1)); //produces flat result array. + } + return $comments; + } + + public function setParent($parent) { + $this->parent = $parent; + } + public function addSubComment($obj) { + $this->children[] = $obj; + } + +} \ No newline at end of file diff --git a/comments.php b/comments.php index b2acc84..e5ebf86 100644 --- a/comments.php +++ b/comments.php @@ -1,113 +1,83 @@ ['onPluginsInitialized', 0] - ]; + public static function getSubscribedEvents() { + return ['onPluginsInitialized' => ['onPluginsInitialized', 0]]; } - - /** - * Initialize form if the page has one. Also catches form processing if user posts the form. - * - * Used by Form plugin < 2.0, kept for backwards compatibility - * - * @deprecated - */ - public function onPageInitialized() - { - /** @var Page $page */ - $page = $this->grav['page']; - if (!$page) { - return; - } - - if ($this->enable) { - $header = $page->header(); - if (!isset($header->form)) { - $header->form = $this->grav['config']->get('plugins.comments.form'); - $page->header($header); - } - } - } - /** * Add the comment form information to the page header dynamically * * Used by Form plugin >= 2.0 */ - public function onFormPageHeaderProcessed(Event $event) - { + public function onFormPageHeaderProcessed(Event $event) { $header = $event['header']; - if ($this->enable) { if (!isset($header->form)) { $header->form = $this->grav['config']->get('plugins.comments.form'); } } - $event->header = $header; } - public function onTwigSiteVariables() { $this->grav['twig']->enable_comments_plugin = $this->enable; $this->grav['twig']->commenting_enabled = $this->commenting_enabled; $this->grav['twig']->pingbacks_enabled = $this->pingbacks_enabled; $this->grav['twig']->comments = $this->fetchComments(); - $this->grav['twig']->pingbacks = $this->fetchPingbacks(); + //$this->grav['twig']->pingbacks = $this->fetchPingbacks(); $this->grav['twig']->akismet_enabled = $this->akismet_enabled; + //$this->grav['twig']->recent_comments = $this->getRecentComments(); //cannot be used for functions with arguments + $function = new Twig_SimpleFunction('recent_comments', [$this, 'getRecentComments']); + $this->grav['twig']->twig()->addFunction($function); + if ($this->config->get('plugins.comments.built_in_css')) { + $this->grav['assets']->addCss('plugin://comments/assets/comments.css'); + } + $this->grav['assets']->add('jquery', 101)->addJs('plugin://comments/assets/comments.js'); } - /** * Determine if the plugin should be enabled based on the enable_on_routes and disable_on_routes config options */ private function calculateEnable() { $uri = $this->grav['uri']; - - $disable_on_routes = (array) $this->config->get('plugins.comments.disable_on_routes'); - $enable_on_routes = (array) $this->config->get('plugins.comments.enable_on_routes'); - + $disable_on_routes = (array)$this->config->get('plugins.comments.disable_on_routes'); + $enable_on_routes = (array)$this->config->get('plugins.comments.enable_on_routes'); + $callback = $this->config->get('plugins.comments.ajax_callback'); $path = $uri->path(); - + if ($callback === $path) { + $this->enable = true; + return; + } if (!in_array($path, $disable_on_routes)) { if (in_array($path, $enable_on_routes)) { $this->enable = true; } else { - foreach($enable_on_routes as $route) { + foreach ($enable_on_routes as $route) { if (Utils::startsWith($path, $route)) { $this->enable = true; break; @@ -115,253 +85,499 @@ class CommentsPlugin extends Plugin } } } - $this->commenting_enabled = $this->grav['config']->get('plugins.comments.commenting'); $this->pingbacks_enabled = $this->grav['config']->get('plugins.comments.pingbacks'); - $this->akismet_enabled = $this->grav['config']->get('plugins.comments.akismet'); } - /** * Frontend side initialization */ - public function initializeFrontend() - { - $this->enable([ - 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], - ]); - + private function initializeFrontend() { + $this->enable(['onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], ]); if ($this->enable) { - $this->enable([ - 'onFormProcessed' => ['onFormProcessed', 0], - 'onFormPageHeaderProcessed' => ['onFormPageHeaderProcessed', 0], - 'onPageInitialized' => ['onPageInitialized', 10], - 'onTwigSiteVariables' => ['onTwigSiteVariables', 0] - ]); + $this->enable(['onPageInitialized' => ['onPageInitialized', 0], 'onFormProcessed' => ['onFormProcessed', 0], 'onFormPageHeaderProcessed' => ['onFormPageHeaderProcessed', 0], 'onTwigSiteVariables' => ['onTwigSiteVariables', 0]]); } - $cache = $this->grav['cache']; $uri = $this->grav['uri']; - //init cache id $this->comments_cache_id = md5('comments-data' . $cache->getKey() . '-' . $uri->url()); - $this->pingbacks_cache_id = md5('pingbacks-data' . $cache->getKey() . '-' . $uri->url()); } - /** * Admin side initialization */ - public function initializeAdmin() - { + private function initializeAdmin() { /** @var Uri $uri */ $uri = $this->grav['uri']; - - $this->enable([ - 'onTwigTemplatePaths' => ['onTwigAdminTemplatePaths', 0], - 'onAdminMenu' => ['onAdminMenu', 0], - 'onDataTypeExcludeFromDataManagerPluginHook' => ['onDataTypeExcludeFromDataManagerPluginHook', 0], - ]); - + $this->enable(['onTwigTemplatePaths' => ['onTwigAdminTemplatePaths', 0], 'onAdminMenu' => ['onAdminMenu', 0], 'onAdminTaskExecute' => ['onAdminTaskExecute', 0], 'onAdminAfterSave' => ['onAdminAfterSave', 0], 'onAdminAfterDelete' => ['onAdminAfterDelete', 0], 'onDataTypeExcludeFromDataManagerPluginHook' => ['onDataTypeExcludeFromDataManagerPluginHook', 0], ]); if (strpos($uri->path(), $this->config->get('plugins.admin.route') . '/' . $this->route) === false) { return; } - $page = $this->grav['uri']->param('page'); $comments = $this->getLastComments($page); - if ($page > 0) { echo json_encode($comments); exit(); } - $this->grav['twig']->comments = $comments; $this->grav['twig']->pages = $this->fetchPages(); } - /** */ - public function onPluginsInitialized() - { + public function onPluginsInitialized() { $this->calculateEnable(); - if ($this->isAdmin()) { $this->initializeAdmin(); } else { $this->initializeFrontend(); } } + /** + * Handle ajax call. + */ + public function onPageInitialized() { + $is_ajax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; + //$callback = $this->config->get('plugins.comments.ajax_callback'); + // Process comment if required + 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. + + } + } + /** + * Validate ajax input before deleting comment + * + * @return boolean[]|string[]|array[][] + */ + private 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]]; + } + } + + /** + * Validate ajax input before adding comment + * + * @return boolean[]|string[]|array[][] + */ + private function addComment($raiseErrors=true) { + if (!$this->active) { + return [false, 'Comment plugin is currently disabled.', [0, 0]]; + } + if (!$this->commenting_enabled) { + return [false, 'Commenting is currently disabled.', [0, 0]]; + } + $language = $this->grav['language']; + if (!$_SERVER["REQUEST_METHOD"] == "POST") { + // Not a POST request, set a 403 (forbidden) response code. + if( $raiseErrors ) { + http_response_code(403); + } + return [false, 'There was a problem with your submission, please try again.', [0, 0]]; + } + // get and filter the data + if (!isset($_POST['data']) || !is_array($_POST['data'])) { + // Set a 400 (bad request) response code and exit. + if( $raiseErrors ) { + http_response_code(400); + } + return [false, 'missing data', [0, 0]]; + } + $post = isset($_POST['data']) ? $_POST['data'] : []; + + $parent = filter_input(INPUT_POST, 'parent', FILTER_SANITIZE_NUMBER_INT); + $name = isset($post['name']) ? filter_var($post['name'], FILTER_SANITIZE_STRING) : null; + $email = isset($post['email']) ? filter_var($post['email'], FILTER_SANITIZE_EMAIL) : null; + $text = isset($post['text']) ? filter_var($post['text'], FILTER_SANITIZE_STRING) : null; + $date = isset($post['date']) ? filter_var($post['date'], FILTER_SANITIZE_STRING) : null; + $site = isset($post['site']) ? filter_var($post['site'], FILTER_SANITIZE_URL) : null; + $lang = isset($post['lang']) ? filter_var($post['lang'], FILTER_SANITIZE_STRING) : null; + $path = isset($post['path']) ? filter_var($post['path'], FILTER_SANITIZE_STRING) : null; + $formname = filter_input(INPUT_POST, 'form-name', FILTER_SANITIZE_STRING); + $formnonce = filter_input(INPUT_POST, 'form-nonce', FILTER_SANITIZE_STRING); + if (!Utils::verifyNonce($formnonce, 'comments')) { + if( $raiseErrors ) { + http_response_code(403); + } + return [false, 'Invalid security nonce', [0, $formnonce]]; + } + // ensure both values are sent + if (is_null($text)) { + // Set a 400 (bad request) response code and exit. + http_response_code(400); + return [false, 'missing text', [0, 0]]; + //return [false, $language->translate('PLUGIN_COMMENTS.FAIL'), $data]; + + } + // sanity checks for parents + if ($parent < 0) { + $parent = 0; + } elseif ($parent > 999) { //TODO: Change to 'exists in list of comment ids + $parent = 0; + } + $ip = $_SERVER['REMOTE_ADDR']; + $lang = $this->grav['language']->getLanguage(); + $path = $this->grav['page']->path(); + $route = $this->grav['page']->route(); + $user = $this->grav['user']->authenticated ? $this->grav['user']->username : ''; + $isAdmin = $this->grav['user']->authorize('admin.login'); + $comment = $this->saveComment($ip, $route, $path, $parent, $lang, $text, $name, $email, $site, $user, $isAdmin); + //$comments = $this->fetchComments(); + $data = array('parent' => $comment['parent'], 'id' => $comment['id'], 'text' => $comment['text'], 'name' => $comment['author'], 'date' => $comment['date'], 'authenticated' => !empty($comment['user']), 'isAdmin' => !empty($comment['isAdmin']), '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'),); + return [true, $language->translate('PLUGIN_COMMENTS.SUCCESS'), $data]; + } + /** + * Handle form processing instructions. + * + * @param Event $event + */ + private 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']) && $comment['parent'] == $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 + + } + //clear cache + $this->grav['cache']->delete($this->comments_cache_id); + $this->grav['cache']->delete($this->pingbacks_cache_id); + return [$entry_removed, $message]; + } + private function validateComment($name, $email, $text, $site) { + if ($this->akismet_enabled) { + $key = $this->grav['config']->get('plugins.comments.akismet_key_api'); + $url_override = $this->grav['config']->get('plugins.comments.akismet_site'); + $url = !empty($url_override) ? $url_override : $_SERVER['HTTP_HOST']; + $akismet = new \Akismet($url, $key); + $akismet->setCommentAuthor($name); + $akismet->setCommentAuthorEmail($email); + $akismet->setCommentAuthorURL($site); + $akismet->setCommentContent($text); + //$akismet->setPermalink($comment->post->permalink); + try { + $approved = !$akismet->isCommentSpam() ? 'true' : 'false'; + } + catch(Exception $e) { + //EventLog::log($e->getMessage(), 'notice', 'comment', 'HabariAkismet'); + // TODO: admin needs to approve comment + $approved = 'pending'; + } + return $approved; + } + return 'true'; + } + /** + * Handle form processing instructions. + * + * @param Event $event + */ + private function saveComment($ip, $route, $path, $parent, $lang, $text, $name, $email, $site = "", $user = "", $isAdmin = false) { + $date = date('D, d M Y H:i:s', time()); + $approved = $this->validateComment($name, $email, $text, $site); + /******************************/ + /** store comments with page **/ + /******************************/ + $localfilename = $path . '/comments.yaml'; + $localfile = CompiledYamlFile::instance($localfilename); + if (file_exists($localfilename)) { + $data = $localfile->content(); + $data['autoincrement']++; + } else { + $data = array('autoincrement' => 1, 'comments' => array()); + } + $localid = $data['autoincrement']; + $newComment = ['id' => $data['autoincrement'], 'ip' => $ip, 'parent' => $parent, 'lang' => $lang, 'text' => $text, 'date' => $date, 'author' => $name, 'email' => $email, 'site' => $site, 'user' => $user, 'approved' => $approved, 'isAdmin' => !empty($isAdmin), ]; + $data['comments'][] = $newComment; + $localfile->save($data); + /**********************************/ + /** store comments in index file **/ + /**********************************/ + $indexfilename = DATA_DIR . 'comments/index.yaml'; + $indexfile = CompiledYamlFile::instance($indexfilename); + if (file_exists($indexfilename)) { + $data = $indexfile->content(); + } else { + $data = array('comments' => array()); + } + $data['comments'][] = ['page' => $route, 'id' => $localid, 'parent' => $parent, 'lang' => $lang, 'text' => $text, 'date' => $date, 'author' => $name, 'email' => $email, 'site' => $site, 'approved' => $approved, ]; + $indexfile->save($data); + //clear cache, don't let incoming spam thrash the cache. + if ($approved == 'true') { + $this->grav['cache']->delete($this->comments_cache_id); + $this->grav['cache']->delete($this->pingbacks_cache_id); + } + return $newComment; + } /** * Handle form processing instructions. * * @param Event $event */ - public function onFormProcessed(Event $event) - { + public function onFormProcessed(Event $event) { $form = $event['form']; $action = $event['action']; $params = $event['params']; - - if (!$this->active) { - return; - } - - if (!$this->commenting_enabled) { - return; - } - switch ($action) { case 'addComment': - $post = isset($_POST['data']) ? $_POST['data'] : []; - - $lang = filter_var(urldecode($post['lang']), FILTER_SANITIZE_STRING); - $path = filter_var(urldecode($post['path']), FILTER_SANITIZE_STRING); - $text = filter_var(urldecode($post['text']), FILTER_SANITIZE_STRING); - $name = filter_var(urldecode($post['name']), FILTER_SANITIZE_STRING); - $email = filter_var(urldecode($post['email']), FILTER_SANITIZE_STRING); - $title = filter_var(urldecode($post['title']), FILTER_SANITIZE_STRING); - $site = isset($post['site']) ? filter_var(urldecode($post['site']), FILTER_SANITIZE_STRING) : ""; - - if (isset($this->grav['user'])) { - $user = $this->grav['user']; - if ($user->authenticated) { - $name = $user->fullname; - $email = $user->email; - } - } - - /** @var Language $language */ - $language = $this->grav['language']; - $lang = $language->getLanguage(); - - if ($this->akismet_enabled) { - $key = $this->grav['config']->get('plugins.comments.akismet_key_api'); - $url_override = $this->grav['config']->get('plugins.comments.akismet_site'); - $url = !empty($url_override) ? $url_override : $_SERVER['HTTP_HOST']; - - $akismet = new \Akismet($url, $key); - $akismet->setCommentAuthor($name); - $akismet->setCommentAuthorEmail($email); - $akismet->setCommentAuthorURL($site); - $akismet->setCommentContent($text); - //$akismet->setPermalink($comment->post->permalink); - try { - $spam = ($akismet->isCommentSpam()) ? 'spam' : 'ham'; - //return; - } catch (Exception $e) { - //EventLog::log($e->getMessage(), 'notice', 'comment', 'HabariAkismet'); - // TODO: admin needs to approve comment - $spam = "pending"; - } - } - - $filename = DATA_DIR . 'comments'; - $filename .= ($lang ? '/' . $lang : ''); - $filename .= $path . '.yaml'; - $file = File::instance($filename); - - if (file_exists($filename)) { - $data = Yaml::parse($file->content()); - - $data['comments'][] = [ - 'text' => $text, - 'date' => date('D, d M Y H:i:s', time()), - 'author' => $name, - 'email' => $email, - 'site' => $site, - 'approved' => ( !isset($spam) || $spam == 'ham' ? 'true' : 'false' ) - ]; - } else { - $data = array( - 'title' => $title, - 'lang' => $lang, - 'comments' => array([ - 'text' => $text, - 'date' => date('D, d M Y H:i:s', time()), - 'author' => $name, - 'email' => $email, - 'site' => $site, - 'approved' => ( !isset($spam) || $spam == 'ham' ? 'true' : 'false' ) - ]) - ); - } - - $file->save(Yaml::dump($data)); - - //clear cache - if (!isset($spam) || $spam == 'ham') - { - $this->grav['cache']->delete($this->comments_cache_id); - $this->grav['cache']->delete($this->pingbacks_cache_id); - } - break; + addComment(false); + break; } } - + /** + * Used to add a recent comments widget. Call {{ recent_comments(123,12) }} specifying an integer representing the result length. + * + * Returns three different arrays with stats and comments. + * + * @param integer $limit max amount of comments in result set + * @param integer $limit_pages max amount of pages in result set + * + * @return array|array|array global stats, page stats, list of recent comments, options + */ + public function getRecentComments($limit, $limit_pages) { + $routes = $this->grav['pages']->routes(); //routes[route] => path + $paths = array_flip($routes); + $cache = $this->grav['cache']; + $options = array('comments_limit' => $limit, 'pages_limit' => $limit_pages,); + //use cached stats if possible + $recent_comments_cache_id = md5('comments-stats' . $cache->getKey()); + if ($recent_comments = $cache->fetch($recent_comments_cache_id)) { + //use cache only if limits are big enough + if ($recent_comments['options']['comments_limit'] >= $options['comments_limit'] && $recent_comments['options']['pages_limit'] >= $options['pages_limit']) { + return $recent_comments; + } + } + $path = PAGES_DIR; + $dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); + $itrFilter = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST); + $filesItr = new \RegexIterator($itrFilter, '/^.+comments\.yaml$/i'); + $files = array(); + $global_stats = array('active_entries' => 0, 'deleted_entries' => 0, 'active_comments' => 0, 'deleted_comments' => 0, 'active_replies' => 0, 'deleted_replies' => 0, 'pages_with_active_entries' => 0,); + $page_stats = array(); + $comments = array(); + foreach ($filesItr as $filepath => $file) { + if ($file->isDir()) { + // this should never trigger as we are looking vor yamls only + + } else { + $route = ''; + $fileFolder = substr($filepath, 0, strlen($filepath) - strlen($file->getFilename()) - 1); + if (!empty($paths[str_replace('/', '\\', $fileFolder) ])) $route = $paths[str_replace('/', '\\', $fileFolder) ]; + if (!empty($paths[str_replace('\\', '/', $fileFolder) ])) $route = $paths[str_replace('\\', '/', $fileFolder) ]; + $page_stats[$filepath] = array('active_entries' => 0, 'deleted_entries' => 0, 'active_comments' => 0, 'deleted_comments' => 0, 'active_replies' => 0, 'deleted_replies' => 0, 'latest_active_entry' => 0, 'route' => $route,); + $localfile = CompiledYamlFile::instance($filepath); + $localcomments = $localfile->content(); + if (!empty($localcomments['comments']) && is_array($localcomments['comments'])) { + foreach ($localcomments['comments'] as $comment) { + if (!empty($comment['deleted'])) { + empty($comment['parent']) ? $page_stats[$filepath]['deleted_comments']++ : $page_stats[$filepath]['deleted_replies']++; + empty($comment['parent']) ? $global_stats['deleted_comments']++ : $global_stats['deleted_replies']++; + $page_stats[$filepath]['deleted_entries']++; + $global_stats['deleted_entries']++; + } else { + empty($comment['parent']) ? $page_stats[$filepath]['active_comments']++ : $page_stats[$filepath]['active_replies']++; + empty($comment['parent']) ? $global_stats['active_comments']++ : $global_stats['active_replies']++; + $page_stats[$filepath]['active_entries']++; + $global_stats['active_entries']++; + //use unix timestamp for comparing and sorting + if (is_int($comment['date'])) { + $time = $comment['date']; + } else { + $time = \DateTime::createFromFormat('D, d M Y H:i:s', $comment['date'])->getTimestamp(); + } + if (empty($page_stats[$filepath]['latest_active_entry']) || $page_stats[$filepath]['latest_active_entry'] < $time) { + $page_stats[$filepath]['latest_active_entry'] = $time; + } + $comments[] = array_merge(array('path' => $filepath, 'route' => $route, 'time' => $time,), $comment); + } + } + } + if (!empty($page_stats[$filepath]['latest_active_entry'])) { + $global_stats['pages_with_active_entries']++; + } + } + } + //most recent comments first + usort($comments, function ($a, $b) { + if ($a['time'] === $b['time']) return 0; + if ($a['time'] < $b['time']) return 1; + return -1; + }); + //most recent pages first + usort($page_stats, function ($a, $b) { + if ($a['latest_active_entry'] === $b['latest_active_entry']) return 0; + if ($a['latest_active_entry'] < $b['latest_active_entry']) return 1; + return -1; + }); + //reduce comments in output to limit + if (!empty($limit) && $limit > 0 && $limit < count($comments)) { + $comments = array_slice($comments, 0, $limit); + } + //reduce pages in output to limit + if (!empty($limit_pages) && $limit_pages > 0 && $limit_pages < count($page_stats)) { + $page_stats = array_slice($page_stats, 0, $limit_pages); + } + //save to cache if enabled + $cache->save($recent_comments_cache_id, ['global_stats' => $global_stats, 'pages' => $page_stats, 'comments' => $comments, 'options' => $options]); + return ['global_stats' => $global_stats, 'pages' => $page_stats, 'comments' => $comments, 'options' => $options]; + } private function getFilesOrderedByModifiedDate($path = '') { $files = []; - if (!$path) { $path = DATA_DIR . 'comments'; } - if (!file_exists($path)) { Folder::mkdir($path); } - - $dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); - $filterItr = new RecursiveFolderFilterIterator($dirItr); - $itr = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST); - + $dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); + $filterItr = new RecursiveFolderFilterIterator($dirItr); + $itr = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST); $itrItr = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST); $filesItr = new \RegexIterator($itrItr, '/^.+\.yaml$/i'); - // Collect files if modified in the last 7 days foreach ($filesItr as $filepath => $file) { $modifiedDate = $file->getMTime(); $sevenDaysAgo = time() - (7 * 24 * 60 * 60); - if ($modifiedDate < $sevenDaysAgo) { continue; } - - $files[] = (object)array( - "modifiedDate" => $modifiedDate, - "fileName" => $file->getFilename(), - "filePath" => $filepath, - "data" => Yaml::parse(file_get_contents($filepath)) - ); + $files[] = (object)array("modifiedDate" => $modifiedDate, "fileName" => $file->getFilename(), "filePath" => $filepath, "data" => Yaml::parse(file_get_contents($filepath))); } - // Traverse folders and recurse foreach ($itr as $file) { if ($file->isDir()) { $this->getFilesOrderedByModifiedDate($file->getPath() . '/' . $file->getFilename()); } } - // Order files by last modified date - usort($files, function($a, $b) { + usort($files, function ($a, $b) { return !($a->modifiedDate > $b->modifiedDate); }); - return $files; } - private function getLastComments($page = 0) { $number = 30; - $files = []; $files = $this->getFilesOrderedByModifiedDate(); $comments = []; - - foreach($files as $file) { + foreach ($files as $file) { $data = Yaml::parse(file_get_contents($file->filePath)); - - for ($i = 0; $i < count($data['comments']); $i++) { + for ($i = 0;$i < count($data['comments']);$i++) { $commentTimestamp = \DateTime::createFromFormat('D, d M Y H:i:s', $data['comments'][$i]['date'])->getTimestamp(); - $data['comments'][$i]['pageTitle'] = $data['title']; $data['comments'][$i]['filePath'] = $file->filePath; $data['comments'][$i]['timestamp'] = $commentTimestamp; @@ -370,44 +586,75 @@ class CommentsPlugin extends Plugin $comments = array_merge($comments, $data['comments']); } } - // Order comments by date - usort($comments, function($a, $b) { + usort($comments, function ($a, $b) { return !($a['timestamp'] > $b['timestamp']); }); - $totalAvailable = count($comments); $comments = array_slice($comments, $page * $number, $number); $totalRetrieved = count($comments); - - return (object)array( - "comments" => $comments, - "page" => $page, - "totalAvailable" => $totalAvailable, - "totalRetrieved" => $totalRetrieved - ); + return (object)array("comments" => $comments, "page" => $page, "totalAvailable" => $totalAvailable, "totalRetrieved" => $totalRetrieved); } - /** * Return the comments associated to the current route */ - private function fetchComments() { + public function fetchComments() { $cache = $this->grav['cache']; //search in cache if ($comments = $cache->fetch($this->comments_cache_id)) { return $comments; } - $lang = $this->grav['language']->getLanguage(); $filename = $lang ? '/' . $lang : ''; - $filename .= $this->grav['uri']->path() . '.yaml'; - - $comments = $this->getDataFromFilename($filename)['comments']; + $filename.= $this->grav['uri']->path() . '.yaml'; + $comments = $this->getDataFromFilename($filename) ['comments']; + $comments = $this->setCommentLevels($comments); //save to cache if enabled $cache->save($this->comments_cache_id, $comments); return $comments; } - + /** + * Return the latest commented pages + */ + private function setCommentLevels($comments) { + if (!is_array($comments)) { + return $comments; + } + $levelsflat = array(); + foreach ($comments as $key => $comment) { + 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(); + foreach ($levelsflat as $id => $parent) { + $parent = $parent['parent']; + if (!isset($levelsflat[$parent])) { + $leveltree[$id] = $levelsflat[$id]['class']; + } else { + $currentParent = $levelsflat[$parent]['class']; + $currentChild = $levelsflat[$id]['class']; + $levelsflat[$id]['class']->setParent($currentParent); + $levelsflat[$parent]['class']->addSubComment($currentChild); + } + } + //youngest comments first (DESC date), only root comments. Keep replies in ASC date order. + //as long as comments are not editable, it is sufficient to reverse order from comment file + $leveltree = array_reverse($leveltree, true); + //reset comment values to nested order + $comments = array(); + foreach ($leveltree as $id => $comment) { + $comments = array_merge($comments, $comment->getContent()); + } + return $comments; + } /** * Return the pingbacks associated to the current route */ @@ -417,42 +664,41 @@ class CommentsPlugin extends Plugin if ($pingbacks = $cache->fetch($this->pingbacks_cache_id)) { return $pingbacks; } - $lang = $this->grav['language']->getLanguage(); $filename = $lang ? '/' . $lang : ''; - $filename .= $this->grav['uri']->path() . '.yaml'; - - $pingbacks = $this->getDataFromFilename($filename)['pingbacks']; + $filename.= $this->grav['uri']->path() . '.yaml'; + $pingbacks = $this->getDataFromFilenameOld($filename) ['pingbacks']; //save to cache if enabled $cache->save($this->pingbacks_cache_id, $pingbacks); return $pingbacks; } - /** * Return the latest commented pages */ private function fetchPages() { $files = []; $files = $this->getFilesOrderedByModifiedDate(); - $pages = []; - - foreach($files as $file) { - $pages[] = [ - 'title' => $file->data['title'], - 'commentsCount' => count($file->data['comments']), - 'lastCommentDate' => date('D, d M Y H:i:s', $file->modifiedDate) - ]; + foreach ($files as $file) { + $pages[] = ['title' => $file->data['title'], 'commentsCount' => count($file->data['comments']), 'lastCommentDate' => date('D, d M Y H:i:s', $file->modifiedDate) ]; } - return $pages; } - - /** * Given a data file route, return the YAML content already parsed */ private function getDataFromFilename($fileRoute) { + //Single item details + //$fileInstance = CompiledYamlFile::instance(DATA_DIR . 'comments/' . $fileRoute); + //Use comment file in page folder + $fileInstance = CompiledYamlFile::instance($this->grav['page']->path() . '/comments.yaml'); + if (!$fileInstance->content()) { + //Item not found + return; + } + return $fileInstance->content(); + } + private function getDataFromFilenameOld($fileRoute) { //Single item details $fileInstance = File::instance(DATA_DIR . 'comments/' . $fileRoute); @@ -464,28 +710,22 @@ class CommentsPlugin extends Plugin return Yaml::parse($fileInstance->content()); } - /** * Add templates directory to twig lookup paths. */ - public function onTwigTemplatePaths() - { + public function onTwigTemplatePaths() { $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; } - /** * Add plugin templates path */ - public function onTwigAdminTemplatePaths() - { + public function onTwigAdminTemplatePaths() { $this->grav['twig']->twig_paths[] = __DIR__ . '/admin/templates'; $this->grav['twig']->akismet_enabled = $this->akismet_enabled; - if ($this->akismet_enabled) { $key = $this->grav['config']->get('plugins.comments.akismet_key_api'); $url_override = $this->grav['config']->get('plugins.comments.akismet_site'); $url = !empty($url_override) ? $url_override : $_SERVER['HTTP_HOST']; - $akismet = new \Akismet($url, $key); if ($akismet->isKeyValid()) { $this->grav['twig']->warning_message = ""; @@ -494,20 +734,78 @@ class CommentsPlugin extends Plugin } } } - + /** + * Handle the Reindex task from the admin + * + * @param Event $e + */ + public function onAdminTaskExecute(Event $e) { + if ($e['method'] == 'taskReindexComments') { + $controller = $e['controller']; + header('Content-type: application/json'); + if (!$controller->authorizeTask('reindexComments', ['admin.configuration', 'admin.super'])) { + $json_response = ['status' => 'error', 'message' => ' Index not created', 'details' => 'Insufficient permissions to reindex the comments index file.']; + echo json_encode($json_response); + exit; + } + /*TODO // disable warnings + error_reporting(1); + // capture content + ob_start(); + $this->gtnt->createIndex(); + ob_get_clean(); + list($status, $msg) = $this->getIndexCount(); + $json_response = [ + 'status' => $status ? 'success' : 'error', + 'message' => ' ' . $msg + ]; + echo json_encode($json_response); + */ + exit; + } + } + /** + * Perform an 'add' or 'update' for comment data as needed + * + * @param $event + * @return bool + */ + public function onAdminAfterSave($event) { + $obj = $event['object']; + if ($obj instanceof Page) { + //nothing to do + //save means, the page changed, but still exists + + } + return true; + } + /** + * Perform an 'add' or 'update' for comment data as needed + * + * @param $event + * @return bool + */ + public function onAdminAfterDelete($event) { + $obj = $event['object']; + if ($obj instanceof Page) { + //TODO $this->deleteComment($obj); + //clear cache + $this->grav['cache']->delete(md5('comments-stats' . $this->grav['cache']->getKey())); + } + return true; + } /** * Add navigation item to the admin plugin */ - public function onAdminMenu() - { + public function onAdminMenu() { $this->grav['twig']->plugins_hooked_nav['PLUGIN_COMMENTS.COMMENTS'] = ['route' => $this->route, 'icon' => 'fa-file-text']; + $options = ['authorize' => 'taskReindexComments', 'hint' => 'reindexes the comments index', 'class' => 'comments-reindex', 'icon' => 'fa-file-text']; + $this->grav['twig']->plugins_quick_tray['PLUGIN_COMMENTS.COMMENTS'] = $options; } - /** * Exclude comments from the Data Manager plugin */ - public function onDataTypeExcludeFromDataManagerPluginHook() - { + public function onDataTypeExcludeFromDataManagerPluginHook() { $this->grav['admin']->dataTypesExcludedFromDataManagerPlugin[] = 'comments'; } -} \ No newline at end of file +} diff --git a/comments.yaml b/comments.yaml index 7c414f2..7b3aaef 100644 --- a/comments.yaml +++ b/comments.yaml @@ -3,14 +3,13 @@ pingbacks: true commenting: true akismet_enabled: true warning_message: "" - +built_in_css: true +ajax_callback: /nested-comments enable_on_routes: - '/blog' - disable_on_routes: - /blog/blog-post-to-ignore - /ignore-this-route - form: name: comments fields: @@ -29,9 +28,9 @@ form: validate: required: true - - name: blah + - name: site label: PLUGIN_COMMENTS.SITE_LABEL - placeholder: "https://leetnightshade.com" + placeholder: type: text validate: required: false @@ -48,10 +47,6 @@ form: process: fillWithCurrentDateTime: true - - name: title - type: hidden - evaluateDefault: grav.page.header.title - - name: lang type: hidden evaluateDefault: grav.language.getLanguage diff --git a/languages.yaml b/languages.yaml index 5c0cc87..46c9fd9 100644 --- a/languages.yaml +++ b/languages.yaml @@ -1,13 +1,22 @@ de: PLUGIN_COMMENTS: + ADD_NEW: Kommentar hinzufügen + ADD_REPLY: Auf Kommentar antworten ADD_COMMENT: Kommentar hinzufügen AKISMET: Akismet AKISMET_KEY_API: Akismet API Key AKISMET_SITE_OVERRIDE: Akismet Site Override + DELETE_COMMENT: Kommentar löschen + REPLY: Antworten + DELETE: Löschen + SUCCESS: "Der Kommentar wurde erfolgreich gespeichert." COMMENTS: Kommentare COMMENTS_NESTED: Nested Comments COMMENTS_NONE: There are no comments yet. COMMENTS_PINGBACKS: Pingbacks + COMMENTS_STATS: Kommentare + RECENT_COMMENTS: Neue Kommentare + RECENT_PAGES: Kommentierte Seiten EMAIL_NOT_CONFIGURED: Email nicht konfiguriert NEW_COMMENT_EMAIL_SUBJECT: 'Neuer Kommentar für %1$s' NEW_COMMENT_EMAIL_BODY: '

Ein neuer Kommentar am %1$s von %3$s (%4$s).

Seite: %2$s

Text: %5$s

' @@ -30,14 +39,23 @@ de: en: PLUGIN_COMMENTS: + ADD_NEW: Add a comment + ADD_REPLY: Reply to comment ADD_COMMENT: Add a comment AKISMET: Akismet AKISMET_KEY_API: Akismet API Key AKISMET_SITE_OVERRIDE: Akismet Site Override + DELETE_COMMENT: Delete comment + REPLY: Reply + DELETE: Delete + SUCCESS: "Comment has been saved successfully." COMMENTS: Comments COMMENTS_NESTED: Nested Comments COMMENTS_NONE: There are no comments yet. COMMENTS_PINGBACKS: Pingbacks + COMMENTS_STATS: Comments + RECENT_COMMENTS: Recent comments + RECENT_PAGES: Commented pages EMAIL_NOT_CONFIGURED: Email not configured NEW_COMMENT_EMAIL_SUBJECT: 'New comment on %1$s' NEW_COMMENT_EMAIL_BODY: '

A new comment was made on %1$s by %3$s (%4$s).

Page: %2$s

Text: %5$s

' diff --git a/templates/partials/comments.form.html.twig b/templates/partials/comments.form.html.twig new file mode 100644 index 0000000..be8d586 --- /dev/null +++ b/templates/partials/comments.form.html.twig @@ -0,0 +1,37 @@ +
+ + {% for field in grav.config.plugins.comments.form.fields %} + {% set value = form.value(field.name) %} + {% if field.evaluateDefault %} + {% set value = evaluate(field.evaluateDefault) %} + {% endif %} + {% if config.plugins.login.enabled and grav.user.authenticated %} + {% if field.name == 'name' %} + + {% elseif field.name == 'email' %} + + {% else %} +
+ {% include "forms/fields/#{field.type}/#{field.type}.html.twig" %} +
+ {% endif %} + {% else %} +
+ {% include "forms/fields/#{field.type}/#{field.type}.html.twig" %} +
+ {% endif %} + {% endfor %} + {% include "forms/fields/formname/formname.html.twig" %} + +
+ {% for button in grav.config.plugins.comments.form.buttons %} + + {% endfor %} +
+ + {{ nonce_field('comments', 'form-nonce')|raw }} +
+ +
{{ form.message }}
diff --git a/templates/partials/comments.html.twig b/templates/partials/comments.html.twig index 8d58c29..c686d73 100644 --- a/templates/partials/comments.html.twig +++ b/templates/partials/comments.html.twig @@ -1,135 +1,67 @@ {% if grav.twig.enable_comments_plugin %} {% set scope = scope ?: 'data.' %} +
+

{{'PLUGIN_COMMENTS.COMMENTS'|t}}

- {% if grav.twig.pingbacks_enabled %} - {% if grav.twig.pingbacks|length %} - {% set comments_visible = false %} - {% for pingback in grav.twig.pingbacks %} - {% if pingback.approved == "true" %} - {% set comments_visible = true %} - {% endif %} - {% endfor %} - - {% if comments_visible %} -

{{'PLUGIN_COMMENTS.COMMENTS_PINGBACKS'|t}}

- - - {% for pingback in grav.twig.pingbacks %} - {% if pingback.approved == "true" %} - - - - {% endif %} - {% endfor %} -
- -
+ {% if grav.twig.commenting_enabled %} + {{'PLUGIN_COMMENTS.ADD_NEW'|t}} {% endif %} - {% endif %} - {% if grav.twig.commenting_enabled %} -

{{'PLUGIN_COMMENTS.ADD_COMMENT'|t}}

+ {% include 'partials/comments.form.html.twig' %} -
- - {% for field in grav.config.plugins.comments.form.fields %} - {% set value = form.value(field.name) %} - {% if field.evaluateDefault %} - {% set value = evaluate(field.evaluateDefault) %} - {% endif %} - {% if config.plugins.login.enabled and grav.user.authenticated %} - {% if field.name == 'name' %} - - {% elseif field.name == 'email' %} - - {% else %} -
- {% include "forms/fields/#{field.type}/#{field.type}.html.twig" %} -
- {% endif %} - {% else %} -
- {% include "forms/fields/#{field.type}/#{field.type}.html.twig" %} -
- {% endif %} - {% endfor %} - {% include "forms/fields/formname/formname.html.twig" %} - -
- {% for button in grav.config.plugins.comments.form.buttons %} - - {% endfor %} -
- - {{ nonce_field('form', 'form-nonce')|raw }} -
- -
{{ form.message }}
- {% endif %} - - {% if grav.twig.comments|length %} - -

{{'PLUGIN_COMMENTS.COMMENTS'|t}}

- - - {% set comments_visible = false %} - {% for comment in grav.twig.comments %} - {% if comment.approved == "true" %} - {% set comments_visible = true %} - - - - {% endif %} - {% endfor %} + + + {% endif %} + {% endfor %} + {% endif %} {% if not comments_visible %} @@ -138,6 +70,6 @@ {% endif %} -
-
-
- -
-
- {% endif %} + +
{% endif %} diff --git a/templates/partials/recentcomments.html.twig b/templates/partials/recentcomments.html.twig new file mode 100644 index 0000000..296535d --- /dev/null +++ b/templates/partials/recentcomments.html.twig @@ -0,0 +1,54 @@ +{# you may set options when using this partial. Example: include 'partials/recentcomments.html.twig' with {'limit': 5, 'pages_limit': 3} #} +{% if grav.twig.enable_comments_plugin %} +

{{'PLUGIN_COMMENTS.COMMENTS_STATS'|t}}

+ {% set stats = recent_comments(limit|default(5), pages_limit|default(3)) %} + {% if stats.global_stats.active_entries %} + {{stats.global_stats.active_entries}} + ( {{stats.global_stats.deleted_entries}}) + - {{stats.global_stats.active_comments}} + ( {{stats.global_stats.deleted_comments}}) + - {{stats.global_stats.active_replies}} + ( {{stats.global_stats.deleted_replies}}) + - {{stats.global_stats.pages_with_active_entries}} + {% endif %} + {% for key, entry in stats.pages %} + {% if loop.first %} +

{{'PLUGIN_COMMENTS.RECENT_PAGES'|t}} (limit {{stats.options.pages_limit}})

+ + {% endif %} + {% endfor %} + {% for key, entry in stats.comments %} + {% if loop.first %} +

{{'PLUGIN_COMMENTS.RECENT_COMMENTS'|t}} (limit {{stats.options.comments_limit}})

+ + {% endif %} + {% endfor %} +{% endif %}