diff --git a/README.md b/README.md index a403d84..7c6b54d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# Grav Comments Plugin +# 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. + +The main goal of this fork is to add support for Akismet. Recaptcha guards from bots, but Akismet helps deal with actual spam content. +--- The **Comments Plugin** for [Grav](http://github.com/getgrav/grav) adds the ability to add comments to pages, and moderate them. diff --git a/blueprints.yaml b/blueprints.yaml index 1daf30b..9caf823 100644 --- a/blueprints.yaml +++ b/blueprints.yaml @@ -1,5 +1,5 @@ name: Comments -version: 1.2.7 +version: 1.2.8 description: Adds a commenting functionality to your site icon: comment author: @@ -29,3 +29,23 @@ form: 0: PLUGIN_ADMIN.DISABLED validate: type: bool + pingbacks: + type: toggle + label: Pingbacks + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + commenting: + type: toggle + label: Commenting + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool diff --git a/comments.php b/comments.php index 7a59543..b1b0b7f 100644 --- a/comments.php +++ b/comments.php @@ -1,6 +1,9 @@ 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(); } /** @@ -100,6 +110,13 @@ class CommentsPlugin extends Plugin } } } + + #$blueprint = $this->getBlueprint(); + #$this->commenting_enabled = $blueprint->get('form/fields/commenting', false, '/'); + #$this->pingbacks_enabled = $blueprint->get('form/fields/pingbacks' , false, '/'); + + $this->commenting_enabled = $this->grav['config']->get('plugins.comments.commenting'); + $this->pingbacks_enabled = $this->grav['config']->get('plugins.comments.pingbacks'); } /** @@ -127,6 +144,8 @@ class CommentsPlugin extends Plugin //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()); } /** @@ -185,16 +204,21 @@ class CommentsPlugin extends Plugin 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); + $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 = filter_var(urldecode($post['site']), FILTER_SANITIZE_STRING); if (isset($this->grav['user'])) { $user = $this->grav['user']; @@ -220,7 +244,8 @@ class CommentsPlugin extends Plugin 'text' => $text, 'date' => date('D, d M Y H:i:s', time()), 'author' => $name, - 'email' => $email + 'email' => $email, + 'site' => $site ]; } else { $data = array( @@ -230,7 +255,8 @@ class CommentsPlugin extends Plugin 'text' => $text, 'date' => date('D, d M Y H:i:s', time()), 'author' => $name, - 'email' => $email + 'email' => $email, + 'site' => $site ]) ); } @@ -239,6 +265,7 @@ class CommentsPlugin extends Plugin //clear cache $this->grav['cache']->delete($this->comments_cache_id); + $this->grav['cache']->delete($this->pingbacks_cache_id); break; } @@ -353,6 +380,26 @@ class CommentsPlugin extends Plugin return $comments; } + /** + * Return the pingbacks associated to the current route + */ + private function fetchPingbacks() { + $cache = $this->grav['cache']; + //search in cache + 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']; + //save to cache if enabled + $cache->save($this->pingbacks_cache_id, $pingbacks); + return $pingbacks; + } + /** * Return the latest commented pages */ diff --git a/comments.yaml b/comments.yaml index 5c09966..3115067 100644 --- a/comments.yaml +++ b/comments.yaml @@ -1,4 +1,6 @@ enabled: true +pingbacks: true +commenting: true enable_on_routes: - '/blog' @@ -6,7 +8,6 @@ enable_on_routes: disable_on_routes: - /blog/blog-post-to-ignore - /ignore-this-route - #- '/blog/daring-fireball-link' form: name: comments @@ -26,6 +27,13 @@ form: validate: required: true + - name: blah + label: PLUGIN_COMMENTS.EMAIL_LABEL + placeholder: "https://leetnightshade.com" + type: text + validate: + required: false + - name: text label: PLUGIN_COMMENTS.MESSAGE_LABEL placeholder: PLUGIN_COMMENTS.MESSAGE_PLACEHOLDER @@ -50,6 +58,9 @@ form: type: hidden evaluateDefault: grav.uri.path + - name: blockme + type: honeypot + # - name: g-recaptcha-response # label: Captcha # type: captcha diff --git a/extern/habariakismet/habariakismet.plugin.php b/extern/habariakismet/habariakismet.plugin.php new file mode 100644 index 0000000..aa6399d --- /dev/null +++ b/extern/habariakismet/habariakismet.plugin.php @@ -0,0 +1,148 @@ +plugin_id()) { + $actions[] = _t('Configure'); + } + + return $actions; + } + + public function action_plugin_ui($plugin_id, $action) + { + if ($plugin_id == $this->plugin_id()) { + switch ($action) { + case _t('Configure'): + + $form = new FormUI(strtolower(get_class($this))); + $form->append('select', 'provider', 'habariakismet__provider', _t('Service')); + $form->provider->options = array( + 'Akismet' => 'Akismet', + 'TypePad AntiSpam' => 'TypePad AntiSpam' + ); + $api_key = $form->append('text', 'api_key', 'habariakismet__api_key', _t('API Key')); + $api_key->add_validator('validate_required'); + $api_key->add_validator(array($this, 'validate_api_key')); + $form->append('submit', 'save', 'Save'); + $form->out(); + break; + } + } + } + + public function validate_api_key($key, $control, $form) + { + $endpoint = ($form->provider->value == 'Akismet') ? self::SERVER_AKISMET : self::SERVER_TYPEPAD; + + $a = new Akismet(Site::get_url('habari'), $key); + $a->setAkismetServer($endpoint); + + if (!$a->isKeyValid()) { + return array(sprintf(_t('Sorry, the %s API key %s is invalid. Please check to make sure the key is entered correctly.'), $form->provider->value, $key)); + } + + return array(); + } + + public function set_priorities() + { + return array( + 'action_comment_insert_before' => 1 + ); + } + + public function action_comment_insert_before(Comment $comment) + { + $api_key = Options::get('habariakismet__api_key'); + $provider = Options::get('habariakismet__provider'); + + if ($api_key == null || $provider == null) { + return; + } + + $endpoint = ($provider == 'Akismet') ? self::SERVER_AKISMET : self::SERVER_TYPEPAD; + + $a = new Akismet(Site::get_url('habari'), $api_key); + $a->setAkismetServer($endpoint); + $a->setCommentAuthor($comment->name); + $a->setCommentAuthorEmail($comment->email); + $a->setCommentAuthorURL($comment->url); + $a->setCommentContent($comment->content); + $a->setPermalink($comment->post->permalink); + + try { + $comment->status = ($a->isCommentSpam()) ? 'spam' : 'ham'; + return; + } catch (Exception $e) { + EventLog::log($e->getMessage(), 'notice', 'comment', 'HabariAkismet'); + } + } + + public function action_admin_moderate_comments($action, $comments, $handler) + { + $false_negatives = 0; + $false_positives = 0; + + $provider = Options::get('habariakismet__provider'); + $endpoint = ($provider == 'Akismet') ? self::SERVER_AKISMET : self::SERVER_TYPEPAD; + + $a = new Akismet(Site::get_url('habari'), Options::get('habariakismet__api_key')); + $a->setAkismetServer($endpoint); + + foreach ($comments as $comment) { + switch ($action) { + case 'spam': + if ($comment->status == Comment::STATUS_APPROVED || $comment->status == Comment::STATUS_UNAPPROVED) { + $a->setCommentAuthor($comment->name); + $a->setCommentAuthorEmail($comment->email); + $a->setCommentAuthorURL($comment->url); + $a->setCommentContent($comment->content); + + $a->submitSpam(); + + $false_negatives++; + } + + break; + case 'approved': + if ($comment->status == Comment::STATUS_SPAM) { + $a->setCommentAuthor($comment->name); + $a->setCommentAuthorEmail($comment->email); + $a->setCommentAuthorURL($comment->url); + $a->setCommentContent($comment->content); + + $a->submitHam(); + + $false_positives++; + } + + break; + } + } + + if ($false_negatives) { + Session::notice(_t('Reported %d false negatives to %s.', array($false_negatives, $provider))); + } + + if ($false_positives) { + Session::notice(_t('Reported %d false positives to %s.', array($false_positives, $provider))); + } + } +} + +?> diff --git a/extern/habariakismet/habariakismet.plugin.xml b/extern/habariakismet/habariakismet.plugin.xml new file mode 100644 index 0000000..b1c59a4 --- /dev/null +++ b/extern/habariakismet/habariakismet.plugin.xml @@ -0,0 +1,10 @@ + + + Habari Akismet + Apache Software License 2.0 + http://habariproject.org + The Habari Community + 0.2 + 98bdb03a-a7e5-4c2a-a875-dcfe1d89f130 + + \ No newline at end of file diff --git a/extern/habariakismet/vendor/Akismet.class.php b/extern/habariakismet/vendor/Akismet.class.php new file mode 100644 index 0000000..3a63922 --- /dev/null +++ b/extern/habariakismet/vendor/Akismet.class.php @@ -0,0 +1,392 @@ +Usage: + * + * $akismet = new Akismet('http://www.example.com/blog/', 'aoeu1aoue'); + * $akismet->setCommentAuthor($name); + * $akismet->setCommentAuthorEmail($email); + * $akismet->setCommentAuthorURL($url); + * $akismet->setCommentContent($comment); + * $akismet->setPermalink('http://www.example.com/blog/alex/someurl/'); + * if($akismet->isCommentSpam()) + * // store the comment but mark it as spam (in case of a mis-diagnosis) + * else + * // store the comment normally + * + * + * Optionally you may wish to check if your WordPress API key is valid as in the example below. + * + * + * $akismet = new Akismet('http://www.example.com/blog/', 'aoeu1aoue'); + * + * if($akismet->isKeyValid()) { + * // api key is okay + * } else { + * // api key is invalid + * } + * + * + * @package akismet + * @name Akismet + * @version 0.4 + * @author Alex Potsides + * @link http://www.achingbrain.net/ + */ +class Akismet + { + private $version = '0.4'; + private $wordPressAPIKey; + private $blogURL; + private $comment; + private $apiPort; + private $akismetServer; + private $akismetVersion; + + // This prevents some potentially sensitive information from being sent accross the wire. + private $ignore = array('HTTP_COOKIE', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED_HOST', + 'HTTP_MAX_FORWARDS', + 'HTTP_X_FORWARDED_SERVER', + 'REDIRECT_STATUS', + 'SERVER_PORT', + 'PATH', + 'DOCUMENT_ROOT', + 'SERVER_ADMIN', + 'QUERY_STRING', + 'PHP_SELF' ); + + /** + * @param string $blogURL The URL of your blog. + * @param string $wordPressAPIKey WordPress API key. + */ + public function __construct($blogURL, $wordPressAPIKey) { + $this->blogURL = $blogURL; + $this->wordPressAPIKey = $wordPressAPIKey; + + // Set some default values + $this->apiPort = 80; + $this->akismetServer = 'rest.akismet.com'; + $this->akismetVersion = '1.1'; + + // Start to populate the comment data + $this->comment['blog'] = $blogURL; + $this->comment['user_agent'] = $_SERVER['HTTP_USER_AGENT']; + + if(isset($_SERVER['HTTP_REFERER'])) { + $this->comment['referrer'] = $_SERVER['HTTP_REFERER']; + } + + /* + * This is necessary if the server PHP5 is running on has been set up to run PHP4 and + * PHP5 concurently and is actually running through a separate proxy al a these instructions: + * http://www.schlitt.info/applications/blog/archives/83_How_to_run_PHP4_and_PHP_5_parallel.html + * and http://wiki.coggeshall.org/37.html + * Otherwise the user_ip appears as the IP address of the PHP4 server passing the requests to the + * PHP5 one... + */ + $this->comment['user_ip'] = $_SERVER['REMOTE_ADDR'] != getenv('SERVER_ADDR') ? $_SERVER['REMOTE_ADDR'] : getenv('HTTP_X_FORWARDED_FOR'); + } + + /** + * Makes a request to the Akismet service to see if the API key passed to the constructor is valid. + * + * Use this method if you suspect your API key is invalid. + * + * @return bool True is if the key is valid, false if not. + */ + public function isKeyValid() { + // Check to see if the key is valid + $response = $this->sendRequest('key=' . $this->wordPressAPIKey . '&blog=' . $this->blogURL, $this->akismetServer, '/' . $this->akismetVersion . '/verify-key'); + return $response[1] == 'valid'; + } + + // makes a request to the Akismet service + private function sendRequest($request, $host, $path) { + $http_request = "POST " . $path . " HTTP/1.0\r\n"; + $http_request .= "Host: " . $host . "\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n"; + $http_request .= "Content-Length: " . strlen($request) . "\r\n"; + $http_request .= "User-Agent: Akismet PHP5 Class " . $this->version . " | Akismet/1.11\r\n"; + $http_request .= "\r\n"; + $http_request .= $request; + + $socketWriteRead = new SocketWriteRead($host, $this->apiPort, $http_request); + $socketWriteRead->send(); + + return explode("\r\n\r\n", $socketWriteRead->getResponse(), 2); + } + + // Formats the data for transmission + private function getQueryString() { + foreach($_SERVER as $key => $value) { + if(!in_array($key, $this->ignore)) { + if($key == 'REMOTE_ADDR') { + $this->comment[$key] = $this->comment['user_ip']; + } else { + $this->comment[$key] = $value; + } + } + } + + $query_string = ''; + + foreach($this->comment as $key => $data) { + if(!is_array($data)) { + $query_string .= $key . '=' . urlencode(stripslashes($data)) . '&'; + } + } + + return $query_string; + } + + /** + * Tests for spam. + * + * Uses the web service provided by {@link http://www.akismet.com Akismet} to see whether or not the submitted comment is spam. Returns a boolean value. + * + * @return bool True if the comment is spam, false if not + * @throws Will throw an exception if the API key passed to the constructor is invalid. + */ + public function isCommentSpam() { + $response = $this->sendRequest($this->getQueryString(), $this->wordPressAPIKey . '.' . $this->akismetServer, '/' . $this->akismetVersion . '/comment-check'); + + if($response[1] == 'invalid' && !$this->isKeyValid()) { + throw new exception('The Wordpress API key passed to the Akismet constructor is invalid. Please obtain a valid one from http://wordpress.com/api-keys/'); + } + + return ($response[1] == 'true'); + } + + /** + * Submit spam that is incorrectly tagged as ham. + * + * Using this function will make you a good citizen as it helps Akismet to learn from its mistakes. This will improve the service for everybody. + */ + public function submitSpam() { + $this->sendRequest($this->getQueryString(), $this->wordPressAPIKey . '.' . $this->akismetServer, '/' . $this->akismetVersion . '/submit-spam'); + } + + /** + * Submit ham that is incorrectly tagged as spam. + * + * Using this function will make you a good citizen as it helps Akismet to learn from its mistakes. This will improve the service for everybody. + */ + public function submitHam() { + $this->sendRequest($this->getQueryString(), $this->wordPressAPIKey . '.' . $this->akismetServer, '/' . $this->akismetVersion . '/submit-ham'); + } + + /** + * To override the user IP address when submitting spam/ham later on + * + * @param string $userip An IP address. Optional. + */ + public function setUserIP($userip) { + $this->comment['user_ip'] = $userip; + } + + /** + * To override the referring page when submitting spam/ham later on + * + * @param string $referrer The referring page. Optional. + */ + public function setReferrer($referrer) { + $this->comment['referrer'] = $referrer; + } + + /** + * A permanent URL referencing the blog post the comment was submitted to. + * + * @param string $permalink The URL. Optional. + */ + public function setPermalink($permalink) { + $this->comment['permalink'] = $permalink; + } + + /** + * The type of comment being submitted. + * + * May be blank, comment, trackback, pingback, or a made up value like "registration" or "wiki". + */ + public function setCommentType($commentType) { + $this->comment['comment_type'] = $commentType; + } + + /** + * The name that the author submitted with the comment. + */ + public function setCommentAuthor($commentAuthor) { + $this->comment['comment_author'] = $commentAuthor; + } + + /** + * The email address that the author submitted with the comment. + * + * The address is assumed to be valid. + */ + public function setCommentAuthorEmail($authorEmail) { + $this->comment['comment_author_email'] = $authorEmail; + } + + /** + * The URL that the author submitted with the comment. + */ + public function setCommentAuthorURL($authorURL) { + $this->comment['comment_author_url'] = $authorURL; + } + + /** + * The comment's body text. + */ + public function setCommentContent($commentBody) { + $this->comment['comment_content'] = $commentBody; + } + + /** + * Defaults to 80 + */ + public function setAPIPort($apiPort) { + $this->apiPort = $apiPort; + } + + /** + * Defaults to rest.akismet.com + */ + public function setAkismetServer($akismetServer) { + $this->akismetServer = $akismetServer; + } + + /** + * Defaults to '1.1' + */ + public function setAkismetVersion($akismetVersion) { + $this->akismetVersion = $akismetVersion; + } +} + +/** + * Utility class used by Akismet + * + * This class is used by Akismet to do the actual sending and receiving of data. It opens a connection to a remote host, sends some data and the reads the response and makes it available to the calling program. + * + * The code that makes up this class originates in the Akismet WordPress plugin, which is {@link http://akismet.com/download/ available on the Akismet website}. + * + * N.B. It is not necessary to call this class directly to use the Akismet class. This is included here mainly out of a sense of completeness. + * + * @package akismet + * @name SocketWriteRead + * @version 0.1 + * @author Alex Potsides + * @link http://www.achingbrain.net/ + */ +class SocketWriteRead { + private $host; + private $port; + private $request; + private $response; + private $responseLength; + private $errorNumber; + private $errorString; + + /** + * @param string $host The host to send/receive data. + * @param int $port The port on the remote host. + * @param string $request The data to send. + * @param int $responseLength The amount of data to read. Defaults to 1160 bytes. + */ + public function __construct($host, $port, $request, $responseLength = 1160) { + $this->host = $host; + $this->port = $port; + $this->request = $request; + $this->responseLength = $responseLength; + $this->errorNumber = 0; + $this->errorString = ''; + } + + /** + * Sends the data to the remote host. + * + * @throws An exception is thrown if a connection cannot be made to the remote host. + */ + public function send() { + $this->response = ''; + + $fs = fsockopen($this->host, $this->port, $this->errorNumber, $this->errorString, 3); + + if($this->errorNumber != 0) { + throw new Exception('Error connecting to host: ' . $this->host . ' Error number: ' . $this->errorNumber . ' Error message: ' . $this->errorString); + } + + if($fs !== false) { + @fwrite($fs, $this->request); + + while(!feof($fs)) { + $this->response .= fgets($fs, $this->responseLength); + } + + fclose($fs); + } + } + + /** + * Returns the server response text + * + * @return string + */ + public function getResponse() { + return $this->response; + } + + /** + * Returns the error number + * + * If there was no error, 0 will be returned. + * + * @return int + */ + public function getErrorNumner() { + return $this->errorNumber; + } + + /** + * Returns the error string + * + * If there was no error, an empty string will be returned. + * + * @return string + */ + public function getErrorString() { + return $this->errorString; + } +} + +?> \ No newline at end of file diff --git a/languages.yaml b/languages.yaml index 812f22d..173ba3c 100644 --- a/languages.yaml +++ b/languages.yaml @@ -24,6 +24,8 @@ en: PLUGIN_COMMENTS: ADD_COMMENT: Add a comment COMMENTS: Comments + COMMENTS_NONE: There are no comments yet. + COMMENTS_PINGBACKS: Pingbacks 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.html.twig b/templates/partials/comments.html.twig index 29d79b9..b4153a0 100644 --- a/templates/partials/comments.html.twig +++ b/templates/partials/comments.html.twig @@ -1,60 +1,114 @@ {% if grav.twig.enable_comments_plugin %} {% set scope = scope ?: 'data.' %} -

{{'PLUGIN_COMMENTS.ADD_COMMENT'|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 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' %} - + + {% for pingback in grav.twig.pingbacks %} + {% if pingback.approved == "true" %} + + + + {% endif %} + {% endfor %} +
+ {{pingback.text}} +
+ {{'PLUGIN_COMMENTS.WRITTEN_ON'|t}} {{pingback.date|e}} {{'PLUGIN_COMMENTS.BY'|t}} + {% if pingback.site %} + {{pingback.author}} + {% else %} + {{pingback.author}} + {% endif %} +
+ {% endif %} + {% endif %} + + {% if grav.twig.commenting_enabled %} +

{{'PLUGIN_COMMENTS.ADD_COMMENT'|t}}

+ + + + {% 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 %} - {% else %} -
- {% include "forms/fields/#{field.type}/#{field.type}.html.twig" %} -
- {% endif %} - {% endfor %} - {% include "forms/fields/formname/formname.html.twig" %} + {% endfor %} + {% include "forms/fields/formname/formname.html.twig" %} -
- {% for button in grav.config.plugins.comments.form.buttons %} - - {% endfor %} -
+
+ {% for button in grav.config.plugins.comments.form.buttons %} + + {% endfor %} +
- {{ nonce_field('form', 'form-nonce')|raw }} -
+ {{ nonce_field('form', 'form-nonce')|raw }} + -
{{ form.message }}
+
{{ form.message }}
+ {% endif %} {% if grav.twig.comments|length %}

{{'PLUGIN_COMMENTS.COMMENTS'|t}}

- {% for comment in grav.twig.comments|array_reverse %} - - - + {% set comments_visible = false %} + {% for comment in grav.twig.comments %} + {% if comment.approved == "true" %} + {% set comments_visible = true %} + + + + {% endif %} {% endfor %} + + {% if not comments_visible %} + + + + {% endif %}
- {{comment.text}} -
- {{'PLUGIN_COMMENTS.WRITTEN_ON'|t}} {{comment.date|e}} {{'PLUGIN_COMMENTS.BY'|t}} {{comment.author}} -
+ {{comment.text}} +
+ {{'PLUGIN_COMMENTS.WRITTEN_ON'|t}} {{comment.date|e}} {{'PLUGIN_COMMENTS.BY'|t}} + {% if comment.site %} + {{comment.author}} + {% else %} + {{comment.author}} + {% endif %} +
+ {{'PLUGIN_COMMENTS.COMMENTS_NONE'|t}} +
{% endif %} {% endif %}