Compare commits

..

114 Commits

Author SHA1 Message Date
leetNightshade 43ae3a78a6 Added nested comment image.
Added nested comment image.
2019-01-04 04:09:38 +00:00
leetNightshade 4698549b88 Changed it so you can only nest on comments with an ID, which means old
comments can't be replied to. OH, I reverted the change from using
CompiledYamlFile because I don't like how the RocketTheme Yaml dump is
setup, it breaks comments up onto multiple lines. Thus, I also removed
the reply button on those comments. You can still attempt to reply to a
comment if you're hacking around the frontend, but as long as the
comment doesn't have an ID it shouldn't work. TODO: Make the backend
comment panel work again.
2019-01-03 19:49:02 -08:00
leetNightshade be0eca0ff5 Merge https://github.com/codeshell/grav-plugin-comments into develop 2019-01-03 16:13:23 -08:00
leetNightshade 94134e2575 Adding gravatar support and nice time display, and classes and div's for
customizing comment formatting.
2018-06-29 17:06:21 -07:00
Ernst 43a70fe64a Merge branch 'develop' of https://github.com/leetNightshade/grav-plugin-comments into develop 2017-12-06 12:59:51 -08:00
Ernst 3632251cea Cleaning up a little, reorganizing. Was Trying to look into recatpcha
support, but decided not to bother, keep things simple with verification
server side for now with no added javascript.
2017-12-06 12:58:44 -08:00
Brian b56c5927f7
Minor readme update. 2017-12-05 17:08:25 -08:00
Brian f6a4204ed2
Akismet support.
Stood up Akismet support.
2017-12-05 17:06:23 -08:00
Ernst 9bff3a3397 Merge branch 'develop' of https://github.com/leetNightshade/grav-plugin-comments into develop 2017-12-05 17:04:27 -08:00
Ernst 6a6b1973a1 Adding basic Akismet stuff. Seems to work. Still working on it, make
sure it's okay.
2017-12-05 17:03:12 -08:00
Brian 19d5ef9f1f
Added screenshot, updated readme.
Other forks don't update readme or show what they did, so I thought I'd take the first [rough] step.
2017-12-04 18:17:55 -08:00
Ernst 607e0fa6d4 Format was messed up. 2017-12-01 17:54:29 -08:00
Ernst a52c1b2f6d Imported Pingbacks can display or be toggled off. You can also display
comments but have adding new ones disabled. TODO: allow disabling
comments on a per page basis.
2017-12-01 17:53:07 -08:00
codeshell ee89c2bdca fix ajax display when posting first comment on a page 2017-10-30 12:53:13 +01:00
codeshell bd1799cc7e add recent comments widget as plugin template (partials/recentcomments.html.twig) 2017-10-30 12:18:50 +01:00
codeshell 9179453c85 fix absolute class path
add recent comments widget as twig function
add save authentication and admin status with comments
2017-10-29 01:49:09 +02:00
Thorsten Witteler 905c04937c + ajax-delete comments via front end (if logged in with super admin priviledges) 2017-10-25 02:58:07 +02:00
Thorsten Witteler b4ab9a5111 finish nested comment implementation 2017-10-24 02:57:44 +02:00
Thorsten Witteler 0936de7c1d remove sorting from twig template (breaks nested comments) 2017-10-23 16:56:13 +02:00
Thorsten Witteler d70f109776 validate ajax input 2017-10-23 13:39:49 +02:00
Thorsten Witteler cace16f606 change nonce action from "form" to "comments" 2017-10-23 13:39:31 +02:00
Thorsten Witteler 868d7b1231 add built in css 2017-10-23 13:37:52 +02:00
Thorsten Witteler de3a683cbe prepare ajax 2017-10-20 15:08:26 +02:00
Thorsten Witteler 0c00764ef5 Merge branch 'ajax-nested-comments' into develop 2017-10-20 14:09:02 +02:00
Thorsten Witteler 7d153816f9 prepare ajax 2017-10-20 14:06:02 +02:00
Thorsten Witteler 6d458f8fa4 add level calculation 2017-10-19 23:06:47 +02:00
Thorsten Witteler 018a42a3dc add level calculation 2017-10-19 22:00:00 +02:00
Thorsten Witteler 1bc97a31e7 nested template 2017-10-19 19:40:02 +02:00
Thorsten Witteler 9387fba6ed nested template 2017-10-19 18:53:40 +02:00
Thorsten Witteler e8417c6a0e add index file to user data
add data files to page folder
2017-10-19 17:42:20 +02:00
Flavio Copes 563d7af096 Merge branch 'release/1.2.7' into develop 2017-05-12 14:55:56 +02:00
Flavio Copes 0a5a7a2e9c Prepare release 2017-05-12 14:55:49 +02:00
Flavio Copes 42ff556420 Fix issue with scope for autofilled values 2017-05-12 14:40:51 +02:00
Flavio Copes 228ac73ba8 Fix comment form processing 2017-05-11 19:42:43 +02:00
Flavio Copes f1b1f74348 Fix type in example config 2017-04-14 16:19:33 +02:00
Flavio Copes fac87e9bb5 Move captcha over email, fix #45 2017-02-26 22:05:26 +01:00
Flavio Copes b3da8123a9 Changelog 2017-02-07 14:01:07 +01:00
tidiview d8e1e2dfe2 created a japanese translation ♪ (#43) 2017-02-07 13:59:35 +01:00
Flavio Copes 57089be565 Merge branch 'release/1.2.6' into develop 2017-01-09 10:13:34 +01:00
Flavio Copes cf3284e540 Prepare release 2017-01-09 10:13:13 +01:00
Flavio Copes 4e88431863 Fix #41 using Comments in a Gantry-powered theme did not escape the comment form token correctly 2017-01-09 10:12:23 +01:00
Andy Miller fbf7ee0bc8 Use existing `Utils::startsWith()` method 2016-09-28 09:39:53 -06:00
Flavio Copes e5e254b1a5 Merge branch 'release/1.2.5' into develop 2016-09-16 09:51:53 +02:00
Flavio Copes 201ffedd18 Prepare release 2016-09-16 09:51:44 +02:00
Flavio Copes 7b4f3a4e8d Fix showing comments older than one week in the "latest comments" view 2016-09-16 09:51:01 +02:00
Flavio Copes c1e3f69a96 Merge branch 'release/1.2.4' into develop 2016-09-15 18:36:45 +02:00
Flavio Copes d2d8e68943 Prepare release 2016-09-15 18:36:33 +02:00
Flavio Copes f945d98cfb Fix missing Twig template error if route is excluded but twig is loaded 2016-09-15 18:08:52 +02:00
Flavio Copes cf31dcefe8 Merge branch 'release/1.2.3' into develop 2016-09-15 16:47:41 +02:00
Flavio Copes e699df4430 Prepare release 2016-09-15 16:47:33 +02:00
Flavio Copes 9ade9bb637 Allow comments to work fine on Form 2.0 too 2016-09-15 15:18:14 +02:00
Teknix 61b7cd43d1 Added Norwegian translation (#36)
Also fixed some padding between the language groups
2016-09-15 11:50:09 +02:00
Flavio Copes 18b3150132 Changelog 2016-08-25 14:52:22 +02:00
Saša Janiška 35a1ac159d add Croatian translation (#34) 2016-08-25 14:51:41 +02:00
Flavio Copes d574f063cc Merge branch 'release/1.2.2' into develop 2016-08-12 13:29:53 +02:00
Flavio Copes 7bec6fa74a Prepare release 2016-08-12 13:29:27 +02:00
Flavio Copes e4e1c79d1a Fix issue in storing comments cache when cache is enabled #33 2016-08-12 13:27:50 +02:00
Flavio Copes c1fe1a6a32 Changelog 2016-08-01 08:37:13 +02:00
John Mica ed82d259af add Romanian language (#31)
I have added the Romanian language to the file and also have added ' ' where I thought they are needed. hope this is fine.
2016-08-01 08:30:50 +02:00
Christian Hanne e3510fbf26 Added check for user object to form submission. (#30) 2016-07-25 17:06:35 +02:00
Flavio Copes 6d10e0c054 Merge branch 'release/1.2.1' into develop 2016-07-19 12:06:20 +02:00
Flavio Copes 4def8b6226 Prepare release 2016-07-19 12:05:55 +02:00
Flavio Copes 6a846da4e3 Merge branch 'master' into develop 2016-07-19 12:05:21 +02:00
Flavio Copes 3a09f47bc6 Fix #28 Check if Login plugin is installed before checking for user object 2016-07-19 12:04:17 +02:00
Flavio Copes 706cd6b778 Merge branch 'release/1.2.0' into develop 2016-07-14 21:53:04 +02:00
Flavio Copes b7b109b67c Merge branch 'release/1.2.0' 2016-07-14 21:53:00 +02:00
Flavio Copes ee89137f81 Prepare release 2016-07-14 21:52:54 +02:00
Flavio Copes ee608539d4 Handle logged in users by not requiring username/email
Still post them as hidden to overcome validation, but still overwrite
the values in backend to avoid manipulation
2016-06-30 14:08:59 +02:00
Flavio Copes 0cf90a74d7 Reset comments form after submit 2016-06-30 11:41:12 +02:00
Flavio Copes 60c8d091e6 Rework a bit the organization of the code. Added cache for comments 2016-06-29 18:56:06 +02:00
gitname 04078ba7e0 Fix typos (#26) 2016-06-03 10:26:16 +02:00
Flavio Copes c0265d5524 Fill changelog 2016-05-30 11:19:22 +02:00
Flavio Copes a95d0abd84 Prevent a missing template problem on ignored routes 2016-05-30 11:16:08 +02:00
Raul E Watson 6a70718fc9 Add Spanish (es) translation (#25) 2016-05-30 10:18:16 +02:00
Renata fd106be103 Add Brazilian Portuguese (pt-br) translation (#23) 2016-05-05 12:56:44 +02:00
Flavio Copes 0605e9450e Merge pull request #21 from janklostermann/patch-1
Enhanced German translation
2016-04-25 20:13:42 +02:00
janklostermann 6b64dcf800 Enhanced German translation 2016-04-25 16:12:30 +02:00
Flavio Copes 75ddc791d4 Merge pull request #20 from MATsxm/patch-2
🇫🇷 update + arranged alphabetically
2016-04-11 17:55:40 +02:00
Marc-Antoine Thevenet 570467430a 🇫🇷 update + arranged alphabetically
🇫🇷 update + arranged alphabetically
2016-04-11 11:40:51 -04:00
Flavio Copes 7014a07879 Drop beta message 2016-03-31 11:10:16 +02:00
Flavio Copes 9d38761d44 Allow translating the comments form 2016-03-24 09:03:28 +01:00
Flavio Copes 3919f7d764 Merge pull request #18 from MATsxm/patch-1
🇫🇷 typos
2016-03-16 20:45:23 +01:00
Marc-Antoine Thevenet afd22bea5b 🇫🇷 typos
🇫🇷 typos
2016-03-16 15:04:47 -04:00
Andy Miller f764c0f8c0 Merge branch 'release/1.1.4' 2016-02-05 14:22:04 -07:00
Andy Miller 8ad309d157 Merge branch 'release/1.1.4' into develop 2016-02-05 14:22:04 -07:00
Andy Miller ee8a0f127a version update 2016-02-05 14:21:55 -07:00
Andy Miller e786144ba3 changelog updated 2016-02-05 13:46:12 -07:00
Flavio Copes acb0b42354 Merge pull request #17 from geraldhansen/develop
add german translation
2016-01-30 10:40:56 +01:00
Gerald Hansen ed101387d4 add german translation 2016-01-30 02:27:02 +01:00
Flavio Copes bacbbd1b57 Use common language strings in blueprints 2016-01-21 14:41:22 +01:00
Karol Orzeł ad9ae0a0c0 Polish translation 2016-01-20 09:44:17 +01:00
Flavio Copes cdee9fa1fa Avoid adding the onTwigTemplatePaths if it's not enabled on this page 2016-01-10 14:59:12 +01:00
Andy Miller 47ee19d48b Merge branch 'release/1.1.3' 2016-01-06 12:36:53 -07:00
Andy Miller 87c6d7eb95 Merge branch 'release/1.1.3' into develop 2016-01-06 12:36:53 -07:00
Andy Miller 440c57434e version update 2016-01-06 12:36:44 -07:00
Flavio Copes 1f46ce7e8b Fix year! 2016-01-06 09:44:42 +01:00
Flavio Copes 9c20e4f6e5 Update changelog 2016-01-06 09:44:23 +01:00
Flavio Copes 44e64c50a1 Merge pull request #15 from khanduras/patch-1
Update README.md
2015-12-21 09:06:18 +01:00
khanduras 7aabf397f6 Update README.md
Minor edit for email plugin
2015-12-20 23:20:04 -08:00
Flavio Copes a6476ab527 Increase priority for onPageInitialized in the comments plugin over the form plugin one 2015-12-20 10:14:51 +01:00
Flavio Copes f25fbd7e20 Disable captcha by default, add instructions on how to add it 2015-12-12 14:28:10 +01:00
Flavio Copes aa551e9cea Merge remote-tracking branch 'origin/develop' into develop 2015-12-11 21:46:40 +01:00
Andy Miller a88f5e053f Merge branch 'release/1.1.1' 2015-12-11 13:15:16 -07:00
Andy Miller af9322b00f Merge branch 'release/1.1.1' into develop 2015-12-11 13:15:16 -07:00
Andy Miller b3c1a1c750 Fix changelog format 2015-12-11 13:15:07 -07:00
Flavio Copes 501ce930a1 Merge branch 'release/1.1.2' into develop 2015-12-11 12:21:35 +01:00
Flavio Copes 6629b505f2 Prepare release 2015-12-11 12:21:11 +01:00
Flavio Copes 825cfe1b1b Fix #12 Avoid double encoding comment text output and author name, already filtered and escaped when stored 2015-12-11 12:19:55 +01:00
Flavio Copes 7b8d4048b1 Merge branch 'release/1.1.1' into develop 2015-12-10 20:37:34 +01:00
Flavio Copes 7858289def Prepare release 2015-12-10 20:36:26 +01:00
Flavio Copes 897451312c Drop autofocus on comments form 2015-12-10 10:49:09 +01:00
Flavio Copes 1a6bb46f86 Merge branch 'release/1.1.0' into develop 2015-11-24 15:35:55 +01:00
Flavio Copes 42ede280e9 Merge branch 'release/1.1.0' 2015-11-24 15:30:21 +01:00
Andy Miller 884049ff22 Merge branch 'release/1.0.0' 2015-10-21 16:34:32 -06:00
14 changed files with 2078 additions and 315 deletions

View File

@ -1,14 +1,106 @@
# v1.2.7
## 05/12/2017
1. [](#improved)
* Added Japanese translation
* Move captcha over email [#45](https://github.com/getgrav/grav-plugin-comments/issues/45)
1. [](#bugfix)
* Fix comment form processing
* Fix issue with scope for autofilled values
# v1.2.6
## 01/09/2017
1. [](#improved)
* Use existing `Utils::startsWith()` method
1. [](#bugfix)
* Fix [#41](https://github.com/getgrav/grav-plugin-comments/issues/41) using Comments in a Gantry-powered theme did not escape the comment form token correctly
# v1.2.5
## 09/16/2016
1. [](#bugfix)
* Fix [#37](https://github.com/getgrav/grav-plugin-comments/issues/37) showing comments older than one week in the "latest comments" view
# v1.2.4
## 09/15/2016
1. [](#bugfix)
* Fix missing Twig template error if route is excluded but twig is loaded
# v1.2.3
## 09/15/2016
1. [](#improved)
* Added Croatian translation
1. [](#bugfix)
* Fix [#35](https://github.com/getgrav/grav-plugin-comments/issues/35) Allow comments to work fine on Form 2.0 too
# v1.2.2
## 08/12/2016
1. [](#improved)
* Added Romanian translation
1. [](#bugfix)
* Fix issue in storing comments cache when cache is enabled [#33](https://github.com/getgrav/grav-plugin-comments/issues/33)
# v1.2.1
## 07/19/2016
1. [](#bugfix)
* Check if Login plugin is installed before checking for user object [#28](https://github.com/getgrav/grav-plugin-comments/issues/28)
# v1.2.0
## 07/14/2016
1. [](#improved)
* Prevent a missing template problem on ignored routes
* Allow to translate the comments form
* Added spanish and brazilian portuguese translations
* Enhanced german, russian and french translations
* Added cache for comments
* Handle logged in users by not requiring username/email
* Reset the comments form after a comment is submitted
# v1.1.4
## 02/05/2016
1. [](#improved)
* Added german and polish
* Avoid listening on onTwigTemplatePaths if not enabled
# v1.1.3
## 01/06/2016
1. [](#improved)
* Disable captcha by default, added instructions on how to enable it
1. [](#bugfix)
* Increase priority for onPageInitialized in the comments plugin over the form plugin one to prevent an issue when saving comments
# v1.1.2
## 12/11/2015
1. [](#improved)
Fix double escaping comments text and author
# v1.1.1
## 12/11/2015
1. [](#improved)
* Drop the autofocus on the comment form
1. [](#bugfix)
* Fix double encoding (#12)
# v1.1.0 # v1.1.0
## 11/24/2015 ## 11/24/2015
1. [](#improved)
Use date instead of gmdate to respect the server local time (thanks @bovisp)
1. [](#improved)
Now works with multilang (thanks @bovisp)
1. [](#new) 1. [](#new)
Added french (@codebee-fr) and russian (@joomline) languages * Added french (@codebee-fr) and russian (@joomline) languages
1. [](#new) * Takes advantage of the new nonce support provided by the Form plugin
Takes advantage of the new nonce support provided by the Form plugin 1. [](#improved)
* Use date instead of gmdate to respect the server local time (thanks @bovisp)
* Now works with multilang (thanks @bovisp)
# v1.0.2 # v1.0.2
## 11/13/2015 ## 11/13/2015
@ -21,10 +113,11 @@
# v1.0.1 # v1.0.1
## 11/11/2015 ## 11/11/2015
1. [](#bugfix)
* Fix error when user/data/comments does not exist
1. [](#improved) 1. [](#improved)
* Use onAdminMenu instead of the deprecated onAdminTemplateNavPluginHook * Use onAdminMenu instead of the deprecated onAdminTemplateNavPluginHook
1. [](#bugfix)
* Fix error when user/data/comments does not exist
# v1.0.0 # v1.0.0
## 10/21/2015 ## 10/21/2015

View File

@ -1,9 +1,21 @@
# 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 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.
Preview with commenting disabled but comment plugin still enabled:
![](http://git.leetnightshade.com/leetNightshade/git.leetnightshade.com/raw/branch/master/grav-plugin-comments/Screenshots/blog.default-theme.png)
Preview with nested commenting:
![](http://git.leetnightshade.com/leetNightshade/git.leetnightshade.com/raw/branch/master/grav-plugin-comments/Screenshots/nested.png)
---
The **Comments Plugin** for [Grav](http://github.com/getgrav/grav) adds the ability to add comments to pages, and moderate them. The **Comments Plugin** for [Grav](http://github.com/getgrav/grav) adds the ability to add comments to pages, and moderate them.
| IMPORTANT!!! This plugin is currently in development as is to be considered a **beta release**. As such, use this in a production environment **at your own risk!**. More features will be added in the future.
# Installation # Installation
The Comments plugin is easy to install with GPM. The Comments plugin is easy to install with GPM.
@ -16,8 +28,6 @@ Or clone from GitHub and put in the `user/plugins/comments` folder.
# Usage # Usage
Edit the
Add `{% include 'partials/comments.html.twig' with {'page': page} %}` to the template file where you want to add comments. Add `{% include 'partials/comments.html.twig' with {'page': page} %}` to the template file where you want to add comments.
For example, in Antimatter, in `templates/item.html.twig`: For example, in Antimatter, in `templates/item.html.twig`:
@ -49,13 +59,16 @@ The comment form will appear on the blog post items matching the enabled routes.
To set the enabled routes, create a `user/config/plugins/comments.yaml` file, copy in it the contents of `user/plugins/comments/comments.yaml` and edit the `enable_on_routes` and `disable_on_routes` options according to your needs. To set the enabled routes, create a `user/config/plugins/comments.yaml` file, copy in it the contents of `user/plugins/comments/comments.yaml` and edit the `enable_on_routes` and `disable_on_routes` options according to your needs.
> Make sure you configured the "Email from" and "Email to" email addresses in the Email plugin with your email address!
# Enabling Recaptcha # Enabling Recaptcha
The plugin comes with Recaptcha integration. To make it work, add your own Recaptcha `site` and `secret` keys the the plugin yaml config file. The plugin comes with Recaptcha integration. To make it work, create a `user/config/plugins/comments.yaml` file, copy in it the contents of `user/plugins/comments/comments.yaml` and uncomment the captcha form field and the captcha validation process.
Make sure you add your own Recaptcha `site` and `secret` keys too.
# Where are the comments stored? # Where are the comments stored?
In the `user/data/comments` folder. They're organized by page route, so every page with a comment has a corresponding file. This enabled a quick load of all the page comments. In the `user/data/comments` folder. They're organized by page route, so every page with a comment has a corresponding file. This enables a quick load of all the page comments.
# Visualize comments # Visualize comments

View File

@ -75,6 +75,13 @@
}); });
</script> </script>
{% if grav.twig.warning_message|length %}
<div class="admin-block">
<h1>{{ "PLUGIN_COMMENTS.WARNINGS"|tu }}</h1>
<p class="center">{{ grav.twig.warning_message }}</p>
</div>
{% endif %}
<h1>Comments in the last 7 days</h1> <h1>Comments in the last 7 days</h1>
<div class="admin-block"> <div class="admin-block">

86
assets/comments.css Normal file
View File

@ -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;
}

216
assets/comments.js Normal file
View File

@ -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("<p>Error: </p>");
commentAlert.append("<p>" + JSON.stringify(status) + "</p>");
commentAlert.append("<p>" + JSON.stringify(error) + "</p>");
commentAlert.append("<p>" + JSON.stringify(title) + "</p>");
});
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 = "<div class='comment comment-level-{{comment.level|e}} comment-flag-new' data-level='{{comment.level}}' data-id='{{comment.id}}' >" +
"<div class='comment-left'>" +
"<img class='comment-object' src='https://www.gravatar.com/avatar/{{comment.email|trim|lower|md5}}?d=identicon' alt='user icon'>" +
"</div>" +
"<div class='comment-body'>" +
"<div class='comment-heading'>" +
"<div class='comment-title'><h4>{{comment.title}}</h4></div>" +
"<div class='comment-reply'><a class='comment-add-reply' href='#'><i class='fa fa-reply' title='{{'PLUGIN_COMMENTS.ADD_REPLY'|t}}'></i> {{'PLUGIN_COMMENTS.REPLY'|t}}</a></div>" +
"<div class='comment-meta'>{{'PLUGIN_COMMENTS.WRITTEN_ON'|t}} {{comment.date|e}} {{'PLUGIN_COMMENTS.BY'|t}} {{comment.author}}</div>" +
"</div>" +
"<div class='comment-text' >" +
"{{comment.text}}" +
"</div>" +
"{{nested}}" +
"</div>" +
"</div>";*/
var newMedia = `<div id="comment-{{comment.id}}" class="comment comment-level-{{comment.level|e}}" data-id="{{comment.id}}" itemtype="http://schema.org/UserComments">
<header class="comment-heading">
<img class="comment-avatar" src="https://www.gravatar.com/avatar/{{comment.email|trim|lower|md5}}?size=20&d=identicon" alt="user icon">
<span class="comment-meta">
{% if comment.site %}
<a href="{{comment.site}}">{{comment.author}}</a>
{% else %}
{{comment.author}}
{% endif %}
<a href="{{uri.url(true)}}#comment-{{comment.id}}" title="Link to this comment" itemprop="url">
<time class="comment-date" datetime="{{comment.date|e}}" itemprop="commentTime">
{{comment.date|nicetime(false)}}
</time>
</a>
</span>
</header>
<div class="comment-body">
<div class="comment-text" >
{{comment.text}}
</div>
{{nested}}
<div class="comment-footer">
<span class="comment-reply">
{% if grav.twig.commenting_enabled %}
<a class="comment-add-reply" href="#"><i class="fa fa-reply" title="{{'PLUGIN_COMMENTS.ADD_REPLY'|t}}"></i> {{'PLUGIN_COMMENTS.REPLY'|t}}</a>
{% endif %}
{% if grav.user.access.admin.super %}
<a class="comment-delete" href="#"><i class="fa fa-trash" title="{{'PLUGIN_COMMENTS.DELETE_COMMENT'|t}}"></i> {{'PLUGIN_COMMENTS.DELETE'|t}}</a>
{% endif %}
</span>
</div>
</div>
</div>`;
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("<p>Error: </p>");
commentAlert.append("<p>" + JSON.stringify(status) + "</p>");
commentAlert.append("<p>" + JSON.stringify(error) + "</p>");
commentAlert.append("<p>" + JSON.stringify(title) + "</p>");
});
posting.always(function() {
//alert("finished, be it successful or not");
});
});
});

View File

@ -1,5 +1,5 @@
name: Comments name: Comments
version: 1.1.0 version: 1.2.8
description: Adds a commenting functionality to your site description: Adds a commenting functionality to your site
icon: comment icon: comment
author: author:
@ -21,11 +21,63 @@ form:
fields: fields:
enabled: enabled:
type: toggle type: toggle
label: Plugin status label: PLUGIN_ADMIN.PLUGIN_STATUS
highlight: 1 highlight: 1
default: 0 default: 0
options: options:
1: Enabled 1: PLUGIN_ADMIN.ENABLED
0: Disabled 0: PLUGIN_ADMIN.DISABLED
validate: validate:
type: bool type: bool
commenting:
type: toggle
label: PLUGIN_COMMENTS.COMMENTS
highlight: 1
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
nested:
type: toggle
label: PLUGIN_COMMENTS.COMMENTS_NESTED
highlight: 1
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
pingbacks:
type: toggle
label: PLUGIN_COMMENTS.COMMENTS_PINGBACKS
highlight: 1
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
akismet:
type: toggle
label: PLUGIN_COMMENTS.AKISMET
highlight: 1
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
akismet_key_api:
type: text
label: PLUGIN_COMMENTS.AKISMET_KEY_API
highlight: 1
default:
options:
akismet_site:
type: text
label: PLUGIN_COMMENTS.AKISMET_SITE_OVERRIDE
highlight: 1
default:

44
class/Comment.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace Grav\Plugin;
class Comment
{
private $id = 0;
private $value = array();
private $parent = null;
private $children = array();
public function __construct($id, $content) {
$this->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;
}
}

View File

@ -1,140 +1,402 @@
<?php <?php
namespace Grav\Plugin; namespace Grav\Plugin;
use Grav\Common\Blueprint;
use Grav\Common\Blueprints;
use Grav\Common\BlueprintSchema;
//use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\Filesystem\RecursiveFolderFilterIterator;
use Grav\Common\GPM\GPM; use Grav\Common\GPM\GPM;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Page\Page; use Grav\Common\Page\Page;
use Grav\Common\Page\Pages; use Grav\Common\Page\Pages;
use Grav\Common\Plugin; use Grav\Common\Plugin;
use Grav\Common\Filesystem\RecursiveFolderFilterIterator; use Grav\Common\Utils;
use Grav\Common\User\User;
use RocketTheme\Toolbox\File\File; use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Twig_SimpleFunction;
class CommentsPlugin extends Plugin require_once PLUGINS_DIR . 'comments/class/Comment.php';
{ require_once 'extern/akismet/Akismet.class.php';
class CommentsPlugin extends Plugin {
protected $route = 'comments'; protected $route = 'comments';
protected $enable = false; protected $enable = false;
protected $commenting_enabled = false;
protected $pingbacks_enabled = false;
protected $comments_cache_id;
protected $pingbacks_cache_id;
protected $akismet_enabled;
/** /**
* @return array * @return array
*/ */
public static function getSubscribedEvents() public static function getSubscribedEvents() {
{ return ['onPluginsInitialized' => ['onPluginsInitialized', 0]];
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0],
'onFormProcessed' => ['onFormProcessed', 0],
'onPageInitialized' => ['onPageInitialized', 0],
'onTwigSiteVariables' => ['onTwigSiteVariables', 0]
];
} }
/** /**
* Initialize form if the page has one. Also catches form processing if user posts the form. * Add the comment form information to the page header dynamically
*
* Used by Form plugin >= 2.0
*/ */
public function onPageInitialized() public function onFormPageHeaderProcessed(Event $event) {
{ $header = $event['header'];
if (!$this->isAdmin()) { if ($this->enable) {
/** @var Page $page */ if (!isset($header->form)) {
$page = $this->grav['page']; $header->form = $this->grav['config']->get('plugins.comments.form');
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);
}
} }
} }
$event->header = $header;
} }
public function onTwigSiteVariables() { public function onTwigSiteVariables() {
if (!$this->isAdmin()) { $this->grav['twig']->enable_comments_plugin = $this->enable;
$this->grav['twig']->enable = $this->enable; $this->grav['twig']->commenting_enabled = $this->commenting_enabled;
$this->grav['twig']->pingbacks_enabled = $this->pingbacks_enabled;
if ($this->enable) { $this->grav['twig']->comments = $this->fetchComments();
$this->grav['twig']->comments = $this->fetchComments(); //$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 $haystack starts with $needle. Credit: http://stackoverflow.com/a/10473026/205039
*/
private function startsWith($haystack, $needle) {
return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE;
}
/** /**
* Determine if the plugin should be enabled based on the enable_on_routes and disable_on_routes config options * Determine if the plugin should be enabled based on the enable_on_routes and disable_on_routes config options
*/ */
private function calculateEnable() { private function calculateEnable() {
$uri = $this->grav['uri']; $uri = $this->grav['uri'];
$disable_on_routes = (array)$this->config->get('plugins.comments.disable_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');
$enable_on_routes = (array) $this->config->get('plugins.comments.enable_on_routes'); $callback = $this->config->get('plugins.comments.ajax_callback');
$path = $uri->path(); $path = $uri->path();
if ($callback === $path) {
$this->enable = true;
return;
}
if (!in_array($path, $disable_on_routes)) { if (!in_array($path, $disable_on_routes)) {
if (in_array($path, $enable_on_routes)) { if (in_array($path, $enable_on_routes)) {
$this->enable = true; $this->enable = true;
} else { } else {
foreach($enable_on_routes as $route) { foreach ($enable_on_routes as $route) {
if ($this->startsWith($path, $route)) { if (Utils::startsWith($path, $route)) {
$this->enable = true; $this->enable = true;
break; break;
} }
} }
} }
} }
$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
*/
private function initializeFrontend() {
$this->enable(['onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], ]);
if ($this->enable) {
$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
*/
private function initializeAdmin() {
/** @var Uri $uri */
$uri = $this->grav['uri'];
$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() {
$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[][]
*/ */
public function onPluginsInitialized() private function addComment($raiseErrors=true) {
{ if (!$this->active) {
if (!$this->isAdmin()) { 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'] : [];
$this->calculateEnable(); $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];
$this->enable([ }
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], // 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);
$localfile = File::instance($localfilename);
if (file_exists($localfilename)) {
//$data = $localfile->content();
$data = Yaml::parse($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);
$localfile->save(Yaml::dump($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);
$localfile->save(Yaml::dump($data));
$entry_removed = true;
$message = "Deleted comment ($id) via path ($path)";
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);
$localfile = File::instance($localfilename);
if (file_exists($localfilename)) {
//$data = $localfile->content();
$data = Yaml::parse($localfile->content());
if (isset($data['autoincrement'])) {
$data['autoincrement']++;
} else {
$data['autoincrement'] = max( sizeof($data['comments']), 1 );
}
} else { } else {
$data = array('autoincrement' => 1, 'comments' => array());
/** @var Uri $uri */
$uri = $this->grav['uri'];
//Admin
$this->enable([
'onTwigTemplatePaths' => ['onTwigAdminTemplatePaths', 0],
'onAdminMenu' => ['onAdminMenu', 0],
'onAdminTemplateNavPluginHook' => ['onAdminMenu', 0], //DEPRECATED
'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();
} }
$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);
$localfile->save(Yaml::dump($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;
} }
/** /**
@ -142,131 +404,159 @@ class CommentsPlugin extends Plugin
* *
* @param Event $event * @param Event $event
*/ */
public function onFormProcessed(Event $event) public function onFormProcessed(Event $event) {
{
$form = $event['form']; $form = $event['form'];
$action = $event['action']; $action = $event['action'];
$params = $event['params']; $params = $event['params'];
if (!$this->active) {
return;
}
switch ($action) { switch ($action) {
case 'addComment': case 'addComment':
$post = !empty($_POST) ? $_POST : []; addComment(false);
break;
$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);
/** @var Language $language */
$language = $this->grav['language'];
$lang = $language->getLanguage();
$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
];
} 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
])
);
}
$file->save(Yaml::dump($data));
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);
$localfile = File::instance($filepath);
//$localcomments = $localfile->content();
$localcomments = Yaml::parse($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 = '') { private function getFilesOrderedByModifiedDate($path = '') {
$files = []; $files = [];
if (!$path) { if (!$path) {
$path = DATA_DIR . 'comments'; //$path = DATA_DIR . 'comments';
$path = $this->grav['page']->path() . '/comments.yaml';
} }
if (!file_exists($path)) { if (!file_exists($path)) {
Folder::mkdir($path); Folder::mkdir($path);
} }
$dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
$dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); $filterItr = new RecursiveFolderFilterIterator($dirItr);
$filterItr = new RecursiveFolderFilterIterator($dirItr); $itr = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST);
$itr = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST);
$itrItr = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST); $itrItr = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST);
$filesItr = new \RegexIterator($itrItr, '/^.+\.yaml$/i'); $filesItr = new \RegexIterator($itrItr, '/^.+\.yaml$/i');
// Collect files if modified in the last 7 days // Collect files if modified in the last 7 days
foreach ($filesItr as $filepath => $file) { foreach ($filesItr as $filepath => $file) {
$modifiedDate = $file->getMTime(); $modifiedDate = $file->getMTime();
$sevenDaysAgo = time() - (7 * 24 * 60 * 60); $sevenDaysAgo = time() - (7 * 24 * 60 * 60);
if ($modifiedDate < $sevenDaysAgo) { if ($modifiedDate < $sevenDaysAgo) {
continue; 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 // Traverse folders and recurse
foreach ($itr as $file) { foreach ($itr as $file) {
if ($file->isDir()) { if ($file->isDir()) {
$this->getFilesOrderedByModifiedDate($file->getPath() . '/' . $file->getFilename()); $this->getFilesOrderedByModifiedDate($file->getPath() . '/' . $file->getFilename());
} }
} }
// Order files by last modified date // Order files by last modified date
usort($files, function($a, $b) { usort($files, function ($a, $b) {
return !($a->modifiedDate > $b->modifiedDate); return !($a->modifiedDate > $b->modifiedDate);
}); });
return $files; return $files;
} }
private function getLastComments($page = 0) { private function getLastComments($page = 0) {
$number = 30; $number = 30;
$files = []; $files = [];
$files = $this->getFilesOrderedByModifiedDate(); $files = $this->getFilesOrderedByModifiedDate();
$comments = []; $comments = [];
foreach ($files as $file) {
foreach($files as $file) {
$data = Yaml::parse(file_get_contents($file->filePath)); $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(); $commentTimestamp = \DateTime::createFromFormat('D, d M Y H:i:s', $data['comments'][$i]['date'])->getTimestamp();
$sevenDaysAgo = time() - (7 * 24 * 60 * 60);
if ($commentTimestamp < $sevenDaysAgo) {
continue;
}
$data['comments'][$i]['pageTitle'] = $data['title']; $data['comments'][$i]['pageTitle'] = $data['title'];
$data['comments'][$i]['filePath'] = $file->filePath; $data['comments'][$i]['filePath'] = $file->filePath;
$data['comments'][$i]['timestamp'] = $commentTimestamp; $data['comments'][$i]['timestamp'] = $commentTimestamp;
@ -275,60 +565,129 @@ class CommentsPlugin extends Plugin
$comments = array_merge($comments, $data['comments']); $comments = array_merge($comments, $data['comments']);
} }
} }
// Order comments by date // Order comments by date
usort($comments, function($a, $b) { usort($comments, function ($a, $b) {
return !($a['timestamp'] > $b['timestamp']); return !($a['timestamp'] > $b['timestamp']);
}); });
$totalAvailable = count($comments); $totalAvailable = count($comments);
$comments = array_slice($comments, $page * $number, $number); $comments = array_slice($comments, $page * $number, $number);
$totalRetrieved = count($comments); $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 * 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';
$filename = $this->grav['page']->path() . '/comments.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();
$commentId = 0;
foreach ($comments as $key => $comment) {
$commentId += 1;
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 {
if (isset($comment['id'])) {
$levelsflat[$comment['id']]['parent'] = $comment['parent'];
$levelsflat[$comment['id']]['class'] = new Comment($comment['id'], $comments[$key]);
} else {
$levelsflat[$commentId]['parent'] = $comment['parent'];
$levelsflat[$commentId]['class'] = new Comment($commentId, $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
*/
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(); $lang = $this->grav['language']->getLanguage();
$filename = $lang ? '/' . $lang : ''; $filename = $lang ? '/' . $lang : '';
$filename .= $this->grav['uri']->path() . '.yaml'; $filename.= $this->grav['uri']->path() . '.yaml';
$pingbacks = $this->getDataFromFilenameOld($filename)['pingbacks'];
return $this->getDataFromFilename($filename)['comments']; //save to cache if enabled
$cache->save($this->pingbacks_cache_id, $pingbacks);
return $pingbacks;
} }
/** /**
* Return the latest commented pages * Return the latest commented pages
*/ */
private function fetchPages() { private function fetchPages() {
$files = []; $files = [];
$files = $this->getFilesOrderedByModifiedDate(); $files = $this->getFilesOrderedByModifiedDate();
$pages = []; $pages = [];
foreach ($files as $file) {
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) ];
$pages[] = [
'title' => $file->data['title'],
'commentsCount' => count($file->data['comments']),
'lastCommentDate' => date('D, d M Y H:i:s', $file->modifiedDate)
];
} }
return $pages; return $pages;
} }
/** /**
* Given a data file route, return the YAML content already parsed * Given a data file route, return the YAML content already parsed
*/ */
private function getDataFromFilename($fileRoute) { 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');
$fileInstance = File::instance($this->grav['page']->path() . '/comments.yaml');
if (!$fileInstance->content()) {
//Item not found
return;
}
//return $fileInstance->content();
return Yaml::parse($fileInstance->content());
}
private function getDataFromFilenameOld($fileRoute) {
//Single item details //Single item details
$fileInstance = File::instance(DATA_DIR . 'comments/' . $fileRoute); $fileInstance = File::instance(DATA_DIR . 'comments/' . $fileRoute);
@ -340,36 +699,102 @@ class CommentsPlugin extends Plugin
return Yaml::parse($fileInstance->content()); return Yaml::parse($fileInstance->content());
} }
/** /**
* Add templates directory to twig lookup paths. * Add templates directory to twig lookup paths.
*/ */
public function onTwigTemplatePaths() public function onTwigTemplatePaths() {
{
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
} }
/** /**
* Add plugin templates path * Add plugin templates path
*/ */
public function onTwigAdminTemplatePaths() public function onTwigAdminTemplatePaths() {
{
$this->grav['twig']->twig_paths[] = __DIR__ . '/admin/templates'; $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 = "";
} else {
$this->grav['twig']->warning_message = sprintf("Akismet API key \"%s\" is invalid for the url \"%s\". Provide a correct url override or make sure you're registered. Please check to make sure the key is entered correctly.", $key, $url);
}
}
} }
/**
* 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' => '<i class="fa fa-warning"></i> 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' => '<i class="fa fa-book"></i> ' . $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 * 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']; $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 * Exclude comments from the Data Manager plugin
*/ */
public function onDataTypeExcludeFromDataManagerPluginHook() public function onDataTypeExcludeFromDataManagerPluginHook() {
{
$this->grav['admin']->dataTypesExcludedFromDataManagerPlugin[] = 'comments'; $this->grav['admin']->dataTypesExcludedFromDataManagerPlugin[] = 'comments';
} }
} }

View File

@ -1,35 +1,43 @@
enabled: true enabled: true
pingbacks: true
commenting: true
akismet_enabled: true
warning_message: ""
built_in_css: true
ajax_callback: /nested-comments
enable_on_routes: enable_on_routes:
- '/blog' - '/blog'
disable_on_routes: disable_on_routes:
- /blog/blog-post-to-ignore - /blog/blog-post-to-ignore
- /ignore-this-route - /ignore-this-route
#- '/blog/daring-fireball-link'
form: form:
name: comments name: comments
fields: fields:
- name: name - name: name
label: Name label: PLUGIN_COMMENTS.NAME_LABEL
placeholder: Enter your name placeholder: PLUGIN_COMMENTS.NAME_PLACEHOLDER
autofocus: on
autocomplete: on autocomplete: on
type: text type: text
validate: validate:
required: true required: true
- name: email - name: email
label: Email label: PLUGIN_COMMENTS.EMAIL_LABEL
placeholder: Enter your email address placeholder: PLUGIN_COMMENTS.EMAIL_PLACEHOLDER
type: email type: email
validate: validate:
required: true required: true
- name: site
label: PLUGIN_COMMENTS.SITE_LABEL
placeholder:
type: text
validate:
required: false
- name: text - name: text
label: Message label: PLUGIN_COMMENTS.MESSAGE_LABEL
placeholder: Enter your message placeholder: PLUGIN_COMMENTS.MESSAGE_PLACEHOLDER
type: textarea type: textarea
validate: validate:
required: true required: true
@ -39,10 +47,6 @@ form:
process: process:
fillWithCurrentDateTime: true fillWithCurrentDateTime: true
- name: title
type: hidden
evaluateDefault: grav.page.header.title
- name: lang - name: lang
type: hidden type: hidden
evaluateDefault: grav.language.getLanguage evaluateDefault: grav.language.getLanguage
@ -51,27 +55,31 @@ form:
type: hidden type: hidden
evaluateDefault: grav.uri.path evaluateDefault: grav.uri.path
- name: g-recaptcha-response - name: blockme
label: Captcha type: honeypot
type: captcha
recatpcha_site_key: e32iojeoi32jeoi32jeoij32oiej32oiej3 # - name: g-recaptcha-response
recaptcha_not_validated: 'Captcha not valid!' # label: Captcha
validate: # type: captcha
required: true # recaptcha_site_key: e32iojeoi32jeoi32jeoij32oiej32oiej3
process: # recaptcha_not_validated: 'Captcha not valid!'
ignore: true # validate:
# required: true
# process:
# ignore: true
buttons: buttons:
- type: submit - type: submit
value: Submit value: PLUGIN_COMMENTS.SUBMIT_COMMENT_BUTTON_TEXT
process: process:
# - captcha:
# recaptcha_secret: ej32oiej23oiej32oijeoi32jeio32je
- email: - email:
subject: "[New Comment] from {{ form.value.name|e }}" subject: PLUGIN_COMMENTS.EMAIL_NEW_COMMENT_SUBJECT
body: "{% include 'forms/data.html.twig' %}" body: "{% include 'forms/data.html.twig' %}"
- captcha:
recatpcha_secret: ej32oiej23oiej32oijeoi32jeio32je
- addComment: - addComment:
- message: Thank you for writing your comment! - message: PLUGIN_COMMENTS.THANK_YOU_MESSAGE
- reset: true

392
extern/akismet/Akismet.class.php vendored Normal file
View File

@ -0,0 +1,392 @@
<?php
/**
* Akismet anti-comment spam service
*
* The class in this package allows use of the {@link http://akismet.com Akismet} anti-comment spam service in any PHP5 application.
*
* This service performs a number of checks on submitted data and returns whether or not the data is likely to be spam.
*
* Please note that in order to use this class, you must have a vaild {@link http://wordpress.com/api-keys/ WordPress API key}. They are free for non/small-profit types and getting one will only take a couple of minutes.
*
* For commercial use, please {@link http://akismet.com/commercial/ visit the Akismet commercial licensing page}.
*
* Please be aware that this class is PHP5 only. Attempts to run it under PHP4 will most likely fail.
*
* See the Akismet class documentation page linked to below for usage information.
*
* @package akismet
* @author Alex Potsides, {@link http://www.achingbrain.net http://www.achingbrain.net}
* @version 0.4
* @copyright Alex Potsides, {@link http://www.achingbrain.net http://www.achingbrain.net}
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*/
/**
* The Akismet PHP5 Class
*
* This class takes the functionality from the Akismet WordPress plugin written by {@link http://photomatt.net/ Matt Mullenweg} and allows it to be integrated into any PHP5 application or website.
*
* The original plugin is {@link http://akismet.com/download/ available on the Akismet website}.
*
* <b>Usage:</b>
* <code>
* $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
* </code>
*
* Optionally you may wish to check if your WordPress API key is valid as in the example below.
*
* <code>
* $akismet = new Akismet('http://www.example.com/blog/', 'aoeu1aoue');
*
* if($akismet->isKeyValid()) {
* // api key is okay
* } else {
* // api key is invalid
* }
* </code>
*
* @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;
}
}
?>

View File

@ -1,48 +1,355 @@
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: '<p>Ein neuer Kommentar am %1$s von %3$s (%4$s).</p><p>Seite: %2$s</p><p>Text: %5$s</p>'
EMAIL_FOOTER: ''
NAME: Name:
EMAIL: Email:
WRITTEN_ON: am
BY: Von
NAME_LABEL: "Name"
NAME_PLACEHOLDER: "Namen eingeben"
EMAIL_LABEL: "Email"
EMAIL_PLACEHOLDER: "Email-Adresse eingeben"
MESSAGE_LABEL: "Kommentar"
MESSAGE_PLACEHOLDER: "Kommentar eingeben"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Absenden"
EMAIL_NEW_COMMENT_SUBJECT: "[Neuer Kommentar] von {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Vielen Dank für den Kommentar!"
WARNINGS: Warnings
en: en:
PLUGIN_COMMENTS: PLUGIN_COMMENTS:
ADD_NEW: Add a comment
ADD_REPLY: Reply to comment
ADD_COMMENT: Add a 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: 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 EMAIL_NOT_CONFIGURED: Email not configured
NEW_COMMENT_EMAIL_SUBJECT: 'New comment on %1$s' NEW_COMMENT_EMAIL_SUBJECT: 'New comment on %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>A new comment was made on %1$s by %3$s (%4$s).</p><p>Page: %2$s</p><p>Text: %5$s</p>' NEW_COMMENT_EMAIL_BODY: '<p>A new comment was made on %1$s by %3$s (%4$s).</p><p>Page: %2$s</p><p>Text: %5$s</p>'
EMAIL_FOOTER: '' EMAIL_FOOTER: ''
NAME: Name: NAME: Name:
EMAIL: Email: EMAIL: Email:
WRITTEN_ON: Written on WRITTEN_ON: on
BY: by BY: By
NAME_LABEL: "Name"
NAME_PLACEHOLDER: "Enter your name"
EMAIL_LABEL: "Email"
EMAIL_PLACEHOLDER: "Enter your email address"
MESSAGE_LABEL: "Comment"
MESSAGE_PLACEHOLDER: "Enter your comment"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Submit"
EMAIL_NEW_COMMENT_SUBJECT: "[New Comment] from {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Thank you for writing your comment!"
WARNINGS: Warnings
es:
PLUGIN_COMMENTS:
ADD_COMMENT: Agregar un comentario
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: Comentarios
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: El Email no está configurado
NEW_COMMENT_EMAIL_SUBJECT: 'Nuevo comentario en %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>Un nuevo comentario se hizo en %1$s por %3$s (%4$s).</p><p>Page: %2$s</p><p>Text: %5$s</p>'
EMAIL_FOOTER: ''
NAME: Nombre:
EMAIL: Email:
WRITTEN_ON: en
BY: Por
NAME_LABEL: "Nombre"
NAME_PLACEHOLDER: "Escriba su nombre"
EMAIL_LABEL: "Email"
EMAIL_PLACEHOLDER: "Escriba su email"
MESSAGE_LABEL: "Comentario"
MESSAGE_PLACEHOLDER: "Escriba su comentario"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Enviar"
EMAIL_NEW_COMMENT_SUBJECT: "[Nuevo comentario] de {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Gracias por escribir su comentario!"
WARNINGS: Warnings
fr: fr:
PLUGIN_COMMENTS: PLUGIN_COMMENTS:
ADD_COMMENT: Ajouter un commentaire ADD_COMMENT: Ajouter un commentaire
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: Commentaires COMMENTS: Commentaires
EMAIL_NOT_CONFIGURED: Email non configurée COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: E-mail non configuré
NEW_COMMENT_EMAIL_SUBJECT: 'Nouveau commentaire sur %1$s' NEW_COMMENT_EMAIL_SUBJECT: 'Nouveau commentaire sur %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>Un nouveau commentaire a été posté sur %1$s par %3$s (%4$s).</p><p>Page: %2$s</p><p>Texte: %5$s</p>' NEW_COMMENT_EMAIL_BODY: '<p>Un nouveau commentaire a été publié sur %1$s par %3$s (%4$s).</p><p>Page : %2$s</p><p>Texte : %5$s</p>'
EMAIL_FOOTER: '' EMAIL_FOOTER: ''
NAME: Nom: NAME: Nom :
EMAIL: E-mail :
WRITTEN_ON: le
BY: Par
NAME_LABEL: "Nom"
NAME_PLACEHOLDER: "Indiquez votre nom"
EMAIL_LABEL: "E-mail"
EMAIL_PLACEHOLDER: "Indiquez votre adresse e-mail"
MESSAGE_LABEL: "Commentaire"
MESSAGE_PLACEHOLDER: "Rédigez votre commentaire"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Envoyer"
EMAIL_NEW_COMMENT_SUBJECT: "[Nouveau commentaire] de {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Merci d'avoir rédigé votre commentaire !"
WARNINGS: Warnings
hr:
PLUGIN_COMMENTS:
ADD_COMMENT: Dodaj komentar
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: Komentari
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: Email adresa nije podešena
NEW_COMMENT_EMAIL_SUBJECT: 'Novi komentar na %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>Novi komentar je napisan na %1$s od %3$s (%4$s).</p><p>Stranica:: %2$s</p><p>Tekst: %5$s</p>'
EMAIL_FOOTER: ''
NAME: Ime:
EMAIL: Email: EMAIL: Email:
WRITTEN_ON: Ecrit le WRITTEN_ON: na
BY: par BY: Od
NAME_LABEL: "Ime"
NAME_PLACEHOLDER: "Unesite ime"
EMAIL_LABEL: "Email adresa"
EMAIL_PLACEHOLDER: "Unesite email adresu"
MESSAGE_LABEL: "Komentar"
MESSAGE_PLACEHOLDER: "Unesite komentar"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Pošalji"
EMAIL_NEW_COMMENT_SUBJECT: "[Novi komentar] od {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Hvala Vam što ste napisali svoj komentar!"
WARNINGS: Warnings
it: it:
PLUGIN_COMMENTS: PLUGIN_COMMENTS:
ADD_COMMENT: Aggiungi un commento ADD_COMMENT: Aggiungi un commento
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: Commenti COMMENTS: Commenti
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: Email non configurata EMAIL_NOT_CONFIGURED: Email non configurata
NEW_COMMENT_EMAIL_SUBJECT: 'Nuovo commento su %1$s' NEW_COMMENT_EMAIL_SUBJECT: 'Nuovo commento su %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>Un nuovo commento è stato postato su %1$s da %3$s (%4$s).</p><p>Pagina: %2$s</p><p>Testo: %5$s</p>' NEW_COMMENT_EMAIL_BODY: '<p>Un nuovo commento è stato postato su %1$s da %3$s (%4$s).</p><p>Pagina: %2$s</p><p>Testo: %5$s</p>'
EMAIL_FOOTER: '' EMAIL_FOOTER: ''
NAME: Nome: NAME: Nome:
EMAIL: Email: EMAIL: Email:
WRITTEN_ON: Scritto il WRITTEN_ON: il
BY: da BY: Da
NAME_LABEL: "Nome"
NAME_PLACEHOLDER: "Inserisci il tuo nome"
EMAIL_LABEL: "Email"
EMAIL_PLACEHOLDER: "Inserisci il tuo indirizzo email"
MESSAGE_LABEL: "Messaggio"
MESSAGE_PLACEHOLDER: "Inserisci il tuo commento"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Invia"
EMAIL_NEW_COMMENT_SUBJECT: "[Nuovo commento] da {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Grazie per il tuo commento!"
WARNINGS: Warnings
ja:
PLUGIN_COMMENTS:
ADD_COMMENT: コメントを追加する
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: コメント
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: メールアドレスは設定さていません
NEW_COMMENT_EMAIL_SUBJECT: '%1$sについて新しいコメント'
NEW_COMMENT_EMAIL_BODY: '<p>新しいコメントが%1$sについて%3$sから(%4$s)書かれた.</p><p>ページー : %2$s</p><p>文書 : %5$s</p>'
EMAIL_FOOTER: ''
NAME: 名前 :
EMAIL: メールアドレス :
WRITTEN_ON:
BY:
NAME_LABEL: "名前"
NAME_PLACEHOLDER: "お名前を"
EMAIL_LABEL: "メールアドレスを"
EMAIL_PLACEHOLDER: "ご自分のメールアドレスをここに..."
MESSAGE_LABEL: "コメント"
MESSAGE_PLACEHOLDER: "コメントをここに"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "送信する"
EMAIL_NEW_COMMENT_SUBJECT: "[新しいコメント]、 {{ form.value.name|e }}から"
THANK_YOU_MESSAGE: "コメントを書いてくださいましてありがとうございました!"
WARNINGS: Warnings
pl:
PLUGIN_COMMENTS:
ADD_COMMENT: Dodaj komentarz
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: Komentarzy
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: Email jest nie skofigurowany
NEW_COMMENT_EMAIL_SUBJECT: 'Nowy komentarz %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>Pojawił się nowy komentarz, napisany %1$s przez %3$s (%4$s).</p><p>Strona: %2$s</p><p>Treść: %5$s</p>'
EMAIL_FOOTER: ''
NAME: Imię:
EMAIL: Email:
WRITTEN_ON: na
BY: Przez
ru: ru:
PLUGIN_COMMENTS: PLUGIN_COMMENTS:
ADD_COMMENT: Добавить комментарий ADD_COMMENT: Добавить комментарий
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: Комментарии COMMENTS: Комментарии
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: Email не настроен EMAIL_NOT_CONFIGURED: Email не настроен
NEW_COMMENT_EMAIL_SUBJECT: 'Новый комментарий к %1$s' NEW_COMMENT_EMAIL_SUBJECT: 'Новый комментарий к %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>Новый комментарий был сделан на %1$s by %3$s (%4$s).</p><p>Страница: %2$s</p><p>Текст: %5$s</p>' NEW_COMMENT_EMAIL_BODY: '<p>Новый комментарий был сделан на %1$s by %3$s (%4$s).</p><p>Страница: %2$s</p><p>Текст: %5$s</p>'
EMAIL_FOOTER: '' EMAIL_FOOTER: ''
NAME: Имя: NAME: Имя:
EMAIL: Email: EMAIL: Email:
WRITTEN_ON: Написан в WRITTEN_ON: на
BY: от BY: От
pt-br:
PLUGIN_COMMENTS:
ADD_COMMENT: Escreva um comentário
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: Comentários
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: E-mail não configurado
NEW_COMMENT_EMAIL_SUBJECT: 'Novo comentário em %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>Um novo comentário foi feito em %1$s por %3$s (%4$s).</p><p>Página: %2$s</p><p>Texto: %5$s</p>'
EMAIL_FOOTER: ''
NAME: Name:
EMAIL: Email:
WRITTEN_ON: em
BY: Por
NAME_LABEL: "Nome"
NAME_PLACEHOLDER: "Escreva seu nome"
EMAIL_LABEL: "E-mail"
EMAIL_PLACEHOLDER: "Escreva seu e-mail. Ex.: seunome@provedor.com.br"
MESSAGE_LABEL: "Comentário"
MESSAGE_PLACEHOLDER: "Escreva seu comentário"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Enviar"
EMAIL_NEW_COMMENT_SUBJECT: "[Novo comentário] de {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Obrigada por enviar seu comentário!"
WARNINGS: Warnings
ro:
PLUGIN_COMMENTS:
ADD_COMMENT: 'Adăugați un comentariu'
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: 'Comentarii'
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: 'Adresa de email nu este configurată'
NEW_COMMENT_EMAIL_SUBJECT: 'Comentariu nou pentru %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>Un nou comentariu a fost adăugat la %1$s de către %3$s (%4$s).</p><p>Pagină: %2$s</p><p>Text: %5$s</p>'
EMAIL_FOOTER: ''
NAME: 'Nume:'
EMAIL: 'Adresă de email:'
WRITTEN_ON: 'pe'
BY: 'De'
NAME_LABEL: "Numele"
NAME_PLACEHOLDER: "Introduceți numele Dvs."
EMAIL_LABEL: "Email"
EMAIL_PLACEHOLDER: "Introduceți adresa Dvs. de email"
MESSAGE_LABEL: "Comentariu"
MESSAGE_PLACEHOLDER: "Scrieți comentariul Dvs."
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Trimiteți"
EMAIL_NEW_COMMENT_SUBJECT: "[Comentariu nou] from {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Vă mulțumim pentru comentariu!"
WARNINGS: Warnings
no:
PLUGIN_COMMENTS:
ADD_COMMENT: Skriv en kommentar
AKISMET: Akismet
AKISMET_KEY_API: Akismet API Key
AKISMET_SITE_OVERRIDE: Akismet Site Override
COMMENTS: Kommentarer
COMMENTS_NESTED: Nested Comments
COMMENTS_NONE: There are no comments yet.
COMMENTS_PINGBACKS: Pingbacks
EMAIL_NOT_CONFIGURED: Epost er ikke konfigurert
NEW_COMMENT_EMAIL_SUBJECT: 'Ny kommentar på %1$s'
NEW_COMMENT_EMAIL_BODY: '<p>En ny kommentar er skrevet på %1$s av %3$s (%4$s).</p><p>Side: %2$s</p><p>Tekst: %5$s</p>'
EMAIL_FOOTER: ''
NAME: Navn:
EMAIL: Epost:
WRITTEN_ON:
BY: Av
NAME_LABEL: "Navn"
NAME_PLACEHOLDER: "Skriv ditt navn"
EMAIL_LABEL: "Epost"
EMAIL_PLACEHOLDER: "Skriv din epost adresse"
MESSAGE_LABEL: "Kommentar"
MESSAGE_PLACEHOLDER: "Skriv din kommentar"
SITE_LABEL: Site
SUBMIT_COMMENT_BUTTON_TEXT: "Send"
EMAIL_NEW_COMMENT_SUBJECT: "[Ny kommentar] fra {{ form.value.name|e }}"
THANK_YOU_MESSAGE: "Takk for din kommentar!"
WARNINGS: Warnings

View File

@ -0,0 +1,37 @@
<form id="comments-form" name="{{ grav.config.plugins.comments.form.name }}" class="comments-form"
action="{{ grav.config.plugins.comments.form.action ? base_url ~ grav.config.plugins.comments.form.action : page.url }}"
method="{{ grav.config.plugins.comments.form.method|upper|default('POST') }}">
{% 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' %}
<input type="hidden" name="{{ (scope ~ field.name)|fieldName }}" value="{{grav.user.fullname}}">
{% elseif field.name == 'email' %}
<input type="hidden" name="{{ (scope ~ field.name)|fieldName }}" value="{{grav.user.email}}">
{% else %}
<div>
{% include "forms/fields/#{field.type}/#{field.type}.html.twig" %}
</div>
{% endif %}
{% else %}
<div>
{% include "forms/fields/#{field.type}/#{field.type}.html.twig" %}
</div>
{% endif %}
{% endfor %}
{% include "forms/fields/formname/formname.html.twig" %}
<div class="buttons">
{% for button in grav.config.plugins.comments.form.buttons %}
<button class="button" type="{{ button.type|default('submit') }}">{{ button.value|t|default('Submit') }}</button>
{% endfor %}
</div>
{{ nonce_field('comments', 'form-nonce')|raw }}
</form>
<div id="comments-alert" class="alert">{{ form.message }}</div>

View File

@ -1,46 +1,75 @@
{% if grav.twig.enable %} {% if grav.twig.enable_comments_plugin %}
{% set scope = scope ?: 'data.' %}
<section id="comments-section">
<h2>{{'PLUGIN_COMMENTS.COMMENTS'|t}}</h2>
<h3>{{'PLUGIN_COMMENTS.ADD_COMMENT'|t}}</h3> {% if grav.twig.commenting_enabled %}
<a class="comment-add-new" href="#"><i class="fa fa-plus" title="{{'PLUGIN_COMMENTS.ADD_NEW'|t}}"></i> {{'PLUGIN_COMMENTS.ADD_NEW'|t}}</a>
{% endif %}
<form name="{{ grav.config.plugins.comments.form.name }}" {% include 'partials/comments.form.html.twig' %}
action="{{ grav.config.plugins.comments.form.action ? base_url ~ grav.config.plugins.comments.form.action : page.url }}"
method="{{ grav.config.plugins.comments.form.method|upper|default('POST') }}">
{% for field in grav.config.plugins.comments.form.fields %}
{% set value = form.value(field.name) %} <div id="comments" class="row comments">
{% if field.evaluateDefault %} {% if grav.twig.comments|length %}
{% set value = evaluate(field.evaluateDefault) %} {% set comments_visible = false %}
{% endif %} {% for comment in grav.twig.comments %}
<div> {% if comment.approved == "true" %}
{% include "forms/fields/#{field.type}/#{field.type}.html.twig" %} <div class="comment-wrapper">
</div> {% set comments_visible = true %}
{% endfor %} {% for level in range(0, comment.level|e) %}
{% if level == comment.level|e %}
<div id="thread-{{level}}" class="comment-thread-top comment-thread">
</div>
{% else %}
<div id="thread-{{level}}" class="comment-thread">
</div>
{% endif %}
{% endfor %}
<div id="comment-{{comment.id}}" class="comment comment-level-{{comment.level|e}}" data-id="{{comment.id}}" itemtype="http://schema.org/UserComments">
<header class="comment-heading">
<img class="comment-avatar" src="https://www.gravatar.com/avatar/{{comment.email|trim|lower|md5}}?size=20&d=identicon" alt="user icon">
<span class="comment-meta">
{% if comment.site %}
<a href="{{comment.site}}">{{comment.author}}</a>
{% else %}
{{comment.author}}
{% endif %}
<a href="{{uri.url(true)}}#comment-{{comment.id}}" title="Link to this comment" itemprop="url">
<time class="comment-date" datetime="{{comment.date|e}}" itemprop="commentTime">
{{comment.date|nicetime(false)}}
</time>
</a>
</span>
</header>
<div class="comment-body">
<div class="comment-text" >
{{comment.text}}
</div>
{{nested}}
<div class="comment-footer">
<span class="comment-reply">
{% if grav.twig.commenting_enabled and comment.id %}
<a class="comment-add-reply" href="#"><i class="fa fa-reply" title="{{'PLUGIN_COMMENTS.ADD_REPLY'|t}}"></i> {{'PLUGIN_COMMENTS.REPLY'|t}}</a>
{% endif %}
{% if grav.user.access.admin.super %}
<a class="comment-delete" href="#"><i class="fa fa-trash" title="{{'PLUGIN_COMMENTS.DELETE_COMMENT'|t}}"></i> {{'PLUGIN_COMMENTS.DELETE'|t}}</a>
{% endif %}
</span>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
<div class="buttons"> {% if not comments_visible %}
{% for button in grav.config.plugins.comments.form.buttons %} <tr>
<button class="button" type="{{ button.type|default('submit') }}">{{ button.value|default('Submit') }}</button> <td>
{% endfor %} {{'PLUGIN_COMMENTS.COMMENTS_NONE'|t}}
</div> </td>
</tr>
{{ nonce_field('form', 'form-nonce') }} {% endif %}
</form> </div>
</section>
<div class="alert">{{ form.message }}</div>
{% if grav.twig.comments|length %}
<h3>{{'PLUGIN_COMMENTS.COMMENTS'|t}}</h3>
<table>
{% for comment in grav.twig.comments|array_reverse %}
<tr>
<td>
{{comment.text|e}}
<br />
{{'PLUGIN_COMMENTS.WRITTEN_ON'|t}} {{comment.date|e}} {{'PLUGIN_COMMENTS.BY'|t}} {{comment.author|e}}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endif %} {% endif %}

View File

@ -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 %}
<h1>{{'PLUGIN_COMMENTS.COMMENTS_STATS'|t}}</h1>
{% set stats = recent_comments(limit|default(5), pages_limit|default(3)) %}
{% if stats.global_stats.active_entries %}
<i class="fa fa-comments" title="active_entries"></i> {{stats.global_stats.active_entries}}
(<i class="fa fa-trash" title="deleted_entries"></i> {{stats.global_stats.deleted_entries}})
- <i class="fa fa-comment" title="active_comments"></i>{{stats.global_stats.active_comments}}
(<i class="fa fa-trash" title="deleted_comments"></i> {{stats.global_stats.deleted_comments}})
- <i class="fa fa-commenting" title="active_replies"></i>{{stats.global_stats.active_replies}}
(<i class="fa fa-trash" title="deleted_replies"></i> {{stats.global_stats.deleted_replies}})
- <i class="fa fa-files-o" title="pages_with_active_entries"></i> {{stats.global_stats.pages_with_active_entries}}
{% endif %}
{% for key, entry in stats.pages %}
{% if loop.first %}
<h2>{{'PLUGIN_COMMENTS.RECENT_PAGES'|t}} (limit {{stats.options.pages_limit}})</h2>
<ul class="fa-ul">
{% endif %}
<li><i class="fa-li fa fa-file" title="{{entry.route}}"></i>
{% if entry.route %}
<a href="{{entry.route}}#comments">
{% endif %}
{{entry.active_entries}}
{% if entry.route %}
</a>
{% endif %}
</li>
{% if loop.last %}
</ul>
{% endif %}
{% endfor %}
{% for key, entry in stats.comments %}
{% if loop.first %}
<h2>{{'PLUGIN_COMMENTS.RECENT_COMMENTS'|t}} (limit {{stats.options.comments_limit}})</h2>
<ul class="fa-ul">
{% endif %}
{% set entry_icon = 'fa-comment' %}
{% if not empty(entry.parent) %}
{% set entry_icon = 'fa-commenting' %}
{% endif %}
<li><i class="fa-li fa {{entry_icon}}" title="{{key}}: {{entry.id}}, {{entry.parent}}"></i>
{% if entry.route %}
<a href="{{entry.route}}#comments">
{% endif %}
{{entry.date}}, {{entry.author}}, {{entry.text|truncate(15)}}
{% if entry.route %}
</a>
{% endif %}
</li>
{% if loop.last %}
</ul>
{% endif %}
{% endfor %}
{% endif %}