diff --git a/lang/en_US.ini b/lang/en_US.ini
index 78a857ed..fa73426c 100644
--- a/lang/en_US.ini
+++ b/lang/en_US.ini
@@ -402,4 +402,31 @@ comment_submission_error_email = "Valid email is required."
comment_submission_error_short = "Comment is required and must be at least 3 characters."
comment_submission_error_spam = "SPAM detected!"
pending_comments = "Pending Comments"
-level = "Level"
\ No newline at end of file
+level = "Level"
+enable_jstime="Enable Javascript and timestamp anti-spam protection"
+jstime_desc="Usually bots dont't use Javascript. Form also checks if submitted between 3 and 600 seconds (preventing bots fast submission)"
+comment_email_admin_awaiting="New comment awaiting moderation"
+comment_email_admin_new="New comment"
+comment_email_subscription_subject = "Subscription confirmation to"
+comment_email_new = "New comment on"
+comment_email_from = "From"
+comment_email_moderate = "Moderate comments"
+comment_email_new_subscribed = "New reply on a subscribed thread"
+comment_email_new_replied = "Someone replied to your comment on"
+comment_email_view_comment = "View comment"
+comment_subscribe_confirmation = "Subscription confirmation to"
+comment_subscribe_thread = "Thread subscription at"
+comment_subscribe_request = "We received a subscription request to a thread at"
+comment_subscribe_never_requested = "If you never visited the site or requested to be notified on thread messages, please ignore this email."
+comment_subscribe_click = "Click"
+comment_subscribe_here = "HERE"
+comment_subscribe_confirm_message = "to confirm your subscription and start receiving notification emails on replies on the thread."
+comment_subscribe_unsubscribe_message = "You can unsubscribe all notifications from"
+comment_subscribe_unsubscribe_anytime = "at any time using this link"
+comment_unsubscribe = "unsubscribe"
+sysmsg_subscribe_success = "Your will receive now new comment notifications on the subscribed threads."
+sysmsg_subscribe_fail = "Something went wrong during subscription verification process."
+sysmsg_unsubscribe_success = "You have successfully unsubscribed from notification emails."
+sysmsg_unsubscribe_fail = "Something wrong during unsubscription process"
+codebtn_copy = "Copy"
+codebtn_copied = "Copied!"
diff --git a/lang/it_IT.ini b/lang/it_IT.ini
index a79cf33b..10288612 100644
--- a/lang/it_IT.ini
+++ b/lang/it_IT.ini
@@ -336,3 +336,70 @@ custom_fields = "Campi personalizzati"
views_counter = "Contatore visualizzazioni"
themes = "Temi"
version = "Versione"
+comments_management = "Gestione Commenti"
+all_comments = "Tutti i commenti"
+pending_moderation = "In attesa di moderazione"
+comment = "Commento"
+post_page = "Articolo/Pagina"
+date = "Data"
+status = "Stato"
+actions = "Azioni"
+published = "Pubblicato"
+pending = "In attesa"
+reply_to_comment = "Rispondi al commento"
+notifications_enabled = "Notifiche abilitate"
+modified = "Modificato"
+publish = "Pubblica"
+edit = "Modifica"
+delete = "Elimina"
+confirm_publish_comment = "Sei sicuro di voler pubblicare questo commento?"
+confirm_delete_comment = "Sei sicuro di voler eliminare questo commento? Questo eliminerà anche tutte le risposte a questo commento."
+no_comments_found = "Nessun commento trovato"
+edit_comment = "Modifica commento"
+name = "Nome"
+email = "E-mail"
+update_comment = "Aggiorna commento"
+cancel = "Annulla"
+comments_settings = "Impostazioni commento"
+general_settings = "Impostazioni generali"
+note = "N.B."
+enable_comments_in_main_config = "Per abilitare i commenti locali, imposta comment.system = \"local\" in config/config.ini"
+comment_moderation = "Moderazione commento"
+require_admin_approval = "I commenti devono essere approvati da un amministratore prima di poter essere pubblicati"
+comments_moderation_desc = "Quando abilitata, i nuovi commenti resteranno in attesa di moderazione e non saranno visibili fino a che non saranno approvati"
+anti_spam_protection = "Protezione Anti-Spam"
+enable_honeypot = "Abilita protezione anti-spam vasetto di miele"
+honeypot_desc = "Il vasetto di miele è un campo invisibile che solo i bot riescono a compilare, aiutando a prevenire lo spam senza interazione dell'utente"
+email_notifications = "Notifiche via E-mail"
+enable_notifications = "Abilita le notifiche"
+send_email_notifications = "Invia le notifiche via e-mail per i nuovi commenti"
+admin_email = "E-mail dell'amministratore"
+admin_email_desc = "Indirizzo E-mail dove ricevere le notifiche dei nuovi commenti"
+smtp_settings = "Impostazioni SMTP"
+enable_smtp = "Abilita SMTP"
+enable_smtp_for_emails = "Abilita SMTP per l'invio delle notifiche via e-mail"
+smtp_host = "Host SMTP"
+smtp_port = "Porta SMTP"
+encryption = "Cifratura"
+smtp_username = "Nome utente SMTP"
+smtp_password = "Password SMTP"
+enter_password = "Inserisci password"
+from_email = "Dall'E-mail"
+from_name = "Dal nome"
+save_settings = "Salva le impostazioni"
+leave_a_comment = "Lascia un commento"
+email_not_published = "la tua e-mail non sarà pubblicata"
+comment_formatting_help = "Si può usare il **testo in grassetto** per la formattazione. Vengono preservate le interruzioni di riga."
+notify_new_comments = "Notificami i nuovi commenti in questa discussione"
+post_reply = "Inserisci risposta"
+post_comment = "Inserisci commento"
+reply = "Rispondi"
+comment_submission_success = "Il tuo commento è stato inserito con successo!"
+comment_submission_moderation = "il tuo commento è stato inserito ed è in attesa di moderazione."
+comment_submission_error = "Si è verificato un errore nell'inserimento del tuo commento. Riprova."
+comment_submission_error_shortname = "Il nome è richiesto e deve essere almeno di 2 caratteri."
+comment_submission_error_email = "È richiesta una e-mail valida."
+comment_submission_error_short = "Il commento è richiesto e deve essere almeno di 3 caratteri."
+comment_submission_error_spam = "SPAM rilevato!"
+pending_comments = "Commenti in attesa"
+level = "Livello"
diff --git a/system/admin/views/comments.html.php b/system/admin/views/comments.html.php
index 7fba747c..148ce1f2 100644
--- a/system/admin/views/comments.html.php
+++ b/system/admin/views/comments.html.php
@@ -162,6 +162,13 @@
+
+
+ >
+
+
+
diff --git a/system/htmly.php b/system/htmly.php
index 78d4b941..6ae4e69a 100644
--- a/system/htmly.php
+++ b/system/htmly.php
@@ -498,7 +498,7 @@
});
post('/edit/password', function() {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$username = from($_REQUEST, 'username');
$new_password = from($_REQUEST, 'password');
@@ -545,7 +545,7 @@
});
post('/edit/mfa', function() {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$username = from($_REQUEST, 'username');
$mfa_secret = from($_REQUEST, 'mfa_secret');
@@ -613,8 +613,8 @@
// Edit the frontpage
get('/edit/frontpage', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
@@ -909,8 +909,8 @@
// Show the static add page
get('/add/page', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
@@ -1152,8 +1152,8 @@
// Show the add category
get('/add/category', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -1242,8 +1242,8 @@
// Show admin/posts
get('/admin/posts', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -1306,8 +1306,8 @@
// Show admin/popular
get('/admin/popular', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -1609,8 +1609,8 @@
// Show admin/pages
get('/admin/pages', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -1660,8 +1660,8 @@
// Show admin/pages
get('/admin/pages/:static', function ($static)
{
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -1734,8 +1734,8 @@
// Show import page
get('/admin/import', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'admin') {
@@ -1834,8 +1834,8 @@
// Show admin/search
get('/admin/search', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
config('views.root', 'system/admin/views');
if (login()) {
if ($role === 'editor' || $role === 'admin' && config('fulltext.search') == "true") {
@@ -1957,8 +1957,8 @@
// Show Config page
get('/admin/config', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
@@ -1995,7 +1995,7 @@
// Submitted Config page data
post('/admin/config', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
@@ -2031,8 +2031,8 @@
// Show Config page
get('/admin/config/custom', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
@@ -2068,7 +2068,7 @@
// Submitted Config page data
post('/admin/config/custom', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
@@ -2103,8 +2103,8 @@
// Show Config page
get('/admin/config/reading', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
@@ -2140,7 +2140,7 @@
// Submitted Config page data
post('/admin/config/reading', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$new_config = array();
$new_Keys = array();
@@ -2173,8 +2173,8 @@
// Show Config page
get('/admin/config/writing', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
@@ -2210,7 +2210,7 @@
// Submitted Config page data
post('/admin/config/writing', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$new_config = array();
$new_Keys = array();
@@ -2243,8 +2243,8 @@
// Show Config page
get('/admin/config/widget', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
@@ -2280,7 +2280,7 @@
// Submitted Config page data
post('/admin/config/widget', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$new_config = array();
$new_Keys = array();
@@ -2316,8 +2316,8 @@
// Show Config page
get('/admin/config/metatags', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
@@ -2354,7 +2354,7 @@
// Submitted Config page data
post('/admin/config/metatags', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$new_config = array();
$new_Keys = array();
@@ -2390,8 +2390,8 @@
// Show Config page
get('/admin/config/security', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
@@ -2427,7 +2427,7 @@
// Submitted Config page data
post('/admin/config/security', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$new_config = array();
$new_Keys = array();
@@ -2499,7 +2499,7 @@
// Submitted Config page data
post('/admin/config/performance', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$new_config = array();
$new_Keys = array();
@@ -2531,8 +2531,8 @@
// Show Backup page
get('/admin/backup', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'admin') {
@@ -2593,8 +2593,8 @@
// Show clear cache page
get('/admin/clear-cache', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -2628,8 +2628,8 @@
// Show Update page
get('/admin/update', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'admin') {
@@ -2663,11 +2663,11 @@
// Show the update now link
get('/admin/update/now/:csrf', function ($CSRF) {
- $proper = is_csrf_proper($CSRF);
+ $proper = is_csrf_proper($CSRF) ?? null;
$updater = new \Kanti\HubUpdater(array(
'name' => 'danpros/htmly',
'prerelease' => !!config("prerelease"),
- ));
+ )) ?? null;
if (login() && $proper && $updater->able()) {
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
@@ -2697,8 +2697,8 @@
// Show Menu builder
get('/admin/menu', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -2733,8 +2733,8 @@
post('/admin/menu', function () {
if (login()) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if ($role === 'editor' || $role === 'admin') {
$json = from($_REQUEST, 'json');
file_put_contents('content/data/menu.json', json_encode($json, JSON_UNESCAPED_UNICODE));
@@ -2750,8 +2750,8 @@
// Manage users page
get('/admin/users', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'admin') {
@@ -2784,8 +2784,8 @@
});
get('/admin/add/user', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'admin') {
@@ -2818,9 +2818,9 @@
});
post('/admin/add/user', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
$username = from($_REQUEST, 'username');
$user_role = from($_REQUEST, 'user-role');
$password = from($_REQUEST, 'password');
@@ -2868,8 +2868,8 @@
});
get('/admin/users/:username/edit', function ($username) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'admin') {
@@ -2905,7 +2905,7 @@
// Submitted Config page data
post('/admin/users/:username/edit', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$username = from($_REQUEST, 'username');
$user_role = from($_REQUEST, 'role-name');
@@ -2937,8 +2937,8 @@
});
get('/admin/users/:username/delete', function ($username) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'admin') {
@@ -2972,12 +2972,12 @@
});
post('/admin/users/:username/delete', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
- $file = from($_REQUEST, 'file');
- $username = from($_REQUEST, 'username');
- $user_role = user('role', $username);
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
+ $file = from($_REQUEST, 'file') ?? null;
+ $username = from($_REQUEST, 'username') ?? null;
+ $user_role = user('role', $username) ?? null;
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if ($proper && login()) {
if ($role === 'admin') {
if ($user_role !== 'admin') {
@@ -2994,7 +2994,6 @@
});
post('/admin/gallery', function () {
-
if (login()) {
$page = from($_REQUEST, 'page');
$images = image_gallery(null, $page, 40);
@@ -3004,8 +3003,8 @@
// Show category page
get('/admin/categories', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -3040,8 +3039,8 @@
// Show the category page
get('/admin/categories/:category', function ($category) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -3101,8 +3100,8 @@
// Show admin/comments - All comments
get('/admin/comments', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login() && ($role === 'admin' || $role === 'editor')) {
config('views.root', 'system/admin/views');
@@ -3139,8 +3138,8 @@
// Show admin/comments/pending - Pending comments
get('/admin/comments/pending', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login() && ($role === 'admin' || $role === 'editor')) {
config('views.root', 'system/admin/views');
@@ -3187,8 +3186,8 @@
// Show admin/comments/settings - Settings page
get('/admin/comments/settings', function () {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login() && $role === 'admin') {
config('views.root', 'system/admin/views');
@@ -3214,7 +3213,7 @@
// Save comments settings
post('/admin/comments/settings', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
@@ -3225,6 +3224,7 @@
// Note: HTML forms convert dots to underscores in POST data
$config['comments.moderation'] = isset($_POST['comments_moderation']) ? 'true' : 'false';
$config['comments.honeypot'] = isset($_POST['comments_honeypot']) ? 'true' : 'false';
+ $config['comments.jstime'] = isset($_POST['comments_jstime']) ? 'true' : 'false';
$config['comments.notify'] = isset($_POST['comments_notify']) ? 'true' : 'false';
$config['comments.mail.enabled'] = isset($_POST['comments_mail_enabled']) ? 'true' : 'false';
@@ -3274,8 +3274,8 @@
// Show edit comment form
get('/admin/comments/edit/:commentfile/:commentid', function ($commentfile, $commentid) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login() && ($role === 'admin' || $role === 'editor')) {
config('views.root', 'system/admin/views');
@@ -3320,7 +3320,7 @@
// Update comment
post('/admin/comments/update/:commentfile/:commentid', function ($commentfile, $commentid) {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
@@ -3626,7 +3626,7 @@
$redir = site_url() . 'admin/themes';
header("location: $redir");
}
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if (login() && $proper) {
$new_config = array();
$new_Keys = array();
@@ -3758,8 +3758,8 @@
// Show edit the category page
get('/category/:category/edit', function ($category) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -3804,7 +3804,7 @@
// Get edited data from category page
post('/category/:category/edit', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if(!login()) {
$login = site_url() . 'login';
@@ -3862,8 +3862,8 @@
// Delete category
get('/category/:category/delete', function ($category) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -3908,7 +3908,7 @@
// Get deleted category data
post('/category/:category/delete', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if ($proper && login()) {
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
@@ -4788,7 +4788,7 @@
// Get deleted data from blog post
post('/'. permalink_type() .'/:name/delete', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if ($proper && login()) {
$file = from($_REQUEST, 'file');
$destination = from($_GET, 'destination');
@@ -5023,8 +5023,8 @@
// Show the add sub static page
get('/:static/add', function ($static) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -5137,8 +5137,8 @@
// Show edit the static page
get('/:static/edit', function ($static) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -5188,7 +5188,7 @@
// Get edited data from static page
post('/:static/edit', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if(!login()) {
$login = site_url() . 'login';
header("location: $login");
@@ -5264,8 +5264,8 @@
// Deleted the static page
get('/:static/delete', function ($static) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -5315,7 +5315,7 @@
// Get deleted data for static page
post('/:static/delete', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if ($proper && login()) {
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
@@ -5437,8 +5437,8 @@
// Edit the sub static page
get('/:static/:sub/edit', function ($static, $sub) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -5496,7 +5496,7 @@
// Submitted data from edit sub static page
post('/:static/:sub/edit', function ($static, $sub) {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if(!login()) {
$login = site_url() . 'login';
header("location: $login");
@@ -5577,8 +5577,8 @@
// Delete sub static page
get('/:static/:sub/delete', function ($static, $sub) {
- $user = $_SESSION[site_url()]['user'];
- $role = user('role', $user);
+ $user = $_SESSION[site_url()]['user'] ?? null;
+ $role = user('role', $user) ?? null;
if (login()) {
config('views.root', 'system/admin/views');
if ($role === 'editor' || $role === 'admin') {
@@ -5636,7 +5636,7 @@
// Submitted data from delete sub static page
post('/:static/:sub/delete', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if ($proper && login()) {
$user = $_SESSION[site_url()]['user'];
$role = user('role', $user);
@@ -6057,7 +6057,7 @@
// Get deleted data from blog post
post('/:year/:month/:name/delete', function () {
- $proper = is_csrf_proper(from($_REQUEST, 'csrf_token'));
+ $proper = is_csrf_proper(from($_REQUEST, 'csrf_token')) ?? null;
if ($proper && login()) {
$file = from($_REQUEST, 'file');
$destination = from($_GET, 'destination');
@@ -6085,6 +6085,7 @@
$parentId = from($_POST, 'parent_id');
$notify = from($_POST, 'notify');
$website = from($_POST, 'website'); // honeypot field
+ $company = from($_POST, 'company'); // antispam js and timestamp field
// Note: $url was also set in json file single comment block, but then it is hard to manage if .md file changes name or path
// introduced instead function get_url_from_file that handle both .md (content) and .json (content/comments)
@@ -6095,7 +6096,8 @@
'comment' => $comment,
'parent_id' => $parentId,
'notify' => $notify,
- 'website' => $website
+ 'website' => $website,
+ 'company' => $company
);
$result = commentInsert($data, $url, null);
diff --git a/system/includes/comments-frontend.php b/system/includes/comments-frontend.php
index ef660019..dfb60ed6 100644
--- a/system/includes/comments-frontend.php
+++ b/system/includes/comments-frontend.php
@@ -28,6 +28,10 @@ function displayCommentsForm($url, $mdfile = null, $parentId = null)
+
+
+
+
*
@@ -43,14 +47,12 @@ function displayCommentsForm($url, $mdfile = null, $parentId = null)
-
@@ -186,7 +188,7 @@ function displayCommentsSection($url, $file = null)
-
diff --git a/system/includes/comments.php b/system/includes/comments.php
index 8e536d40..c8262ea9 100644
--- a/system/includes/comments.php
+++ b/system/includes/comments.php
@@ -343,6 +343,26 @@ function buildCommentTree($comments, $parentId = null, $level = 0)
return $tree;
}
+
+/**
+ * Calculate seconds difference from now
+ *
+ * @param int/string $timestamp
+ * @return difference in seconds
+ */
+function secondsGenerationSubmit($timestamp) {
+ if (!is_numeric($timestamp)) {
+ return null; // invalid value
+ }
+
+ $timestampJS = (int) $timestamp;
+ $timestampServer = time();
+
+ return $timestampServer - $timestampJS;
+}
+
+
+
/**
* Validate comment data
*
@@ -375,6 +395,13 @@ function validateComment($data)
}
}
+ // Validate js and time (if enabled) - minimum 2 seconds, maximum 600 seconds
+ if (comments_config('comments.jstime') === 'true') {
+ if (!$data['company'] || secondsGenerationSubmit($data['company']) < 3 || secondsGenerationSubmit($data['company']) > 3600) {
+ $errors[] = 'comment_submission_error_spam';
+ }
+ }
+
return array(
'valid' => empty($errors),
'errors' => $errors
@@ -442,9 +469,21 @@ function commentInsert($data, $url, $mdfile = null)
'message' => 'comment_submission_error'
);
}
+
+ // Subscription handling
+ if ($comment['notify']) {
+ setSubscription($comment['email'], 'subscribe');
+ }
+
+ // Clearing cache if comment is published, otherwise doesn't display on page
+ if ($comment['published']) {
+ rebuilt_cache('all');
+ clear_cache();
+ }
+
- // Send notifications
- sendCommentNotifications($url, $comment, $comments);
+ // Send notifications - notify admin always, notify subscribers only if published
+ sendCommentNotifications($url, $comment, $comments, true, $comment['published']);
return array(
'success' => true,
@@ -453,6 +492,190 @@ function commentInsert($data, $url, $mdfile = null)
);
}
+
+
+// action can be subscribe, confirm, unsubscribe
+function setSubscription($email, $action) {
+ $subscriptions_dir = 'content/comments/.subscriptions';
+ if (!is_dir($subscriptions_dir)) {
+ mkdir($subscriptions_dir);
+ }
+ $subscription_file = $subscriptions_dir . '/' . encryptEmailForFilename($email, comments_config('comments.salt'));
+
+ $subscription = getSubscription($email);
+
+ if ($action == 'subscribe') {
+ if ($subscription['status'] == 'subscribed') {
+ return true;
+ }
+ elseif ($subscription['status'] == 'waiting') {
+ sendSubscriptionEmail($email);
+ }
+ else {
+ $subscription['status'] = 'waiting';
+ $json = json_encode($subscription, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ file_put_contents($subscription_file, $json);
+ sendSubscriptionEmail($email);
+ return true;
+ }
+
+ }
+ elseif ($action == 'confirm' && $subscription['status'] == 'waiting') {
+ $subscription['status'] = 'subscribed';
+ $json = json_encode($subscription, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ file_put_contents($subscription_file, $json);
+ return true;
+ }
+ elseif ($action == 'unsubscribe') {
+ @unlink($subscription_file);
+ return true;
+ }
+ else {
+ // nothing here
+ return false;
+ }
+}
+
+
+// returns array
+function getSubscription($email) {
+ $subscriptions_dir = 'content/comments/.subscriptions';
+ $subscription_file = $subscriptions_dir . '/' . encryptEmailForFilename($email, comments_config('comments.salt'));
+ if (!file_exists($subscription_file)) {
+ $subscription['status'] = 'no';
+ $subscription['date'] = date('Y-m-d H:i:s');
+ $subscription['email'] = $email;
+ return $subscription;
+ }
+ else {
+ $subscription = json_decode(file_get_data($subscription_file), true);
+ return $subscription;
+ }
+}
+
+
+
+function confirmSubscription($filename) {
+ $subscriptions_dir = 'content/comments/.subscriptions';
+ $subscription_file = $subscriptions_dir . '/' . $filename;
+ if (sanitizedSubscriptionFile($filename) && file_exists($subscription_file)) {
+ $subscription = json_decode(file_get_data($subscription_file), true);
+ setSubscription($subscription['email'], 'confirm');
+ return true;
+ }
+ return false;
+}
+
+
+function sanitizedSubscriptionFile($filename) {
+ // no path traversal, sanitizing filename
+ $filename = basename($filename);
+ if (!preg_match('/^[a-zA-Z0-9._-]+$/', $filename)) {
+ return false;
+ }
+
+ $subscriptions_dir = 'content/comments/.subscriptions';
+ $subscription_file = $subscriptions_dir . '/' . $filename;
+
+ // check if path is invalid
+ $real_file = realpath($subscription_file);
+ $real_dir = realpath($subscriptions_dir);
+
+ if ($real_file === false || $real_dir === false) {
+ return false;
+ }
+
+ // check if path outside .subscriptions dir (we are DELETING files!)
+ if (strpos($real_file, $real_dir . DIRECTORY_SEPARATOR) !== 0) {
+ return false;
+ }
+
+ return true;
+
+}
+
+
+
+function deleteSubscription($filename) {
+ $subscriptions_dir = 'content/comments/.subscriptions';
+ $subscription_file = $subscriptions_dir . '/' . $filename;
+
+ if (sanitizedSubscriptionFile($filename) && file_exists($subscription_file)) {
+ @unlink($subscription_file);
+ return true;
+ }
+ return false;
+}
+
+
+
+
+function encryptEmailForFilename(string $email, string $secretKey) {
+ // Normalize email
+ $email = strtolower(trim($email));
+ // Create HMAC hash
+ $hash = hash_hmac('sha256', $email, $secretKey, true);
+
+ // URL-safe Base64 (filename-safe)
+ $safe = rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
+ return $safe;
+}
+
+
+
+function sendSubscriptionEmail($email) {
+ try {
+ $mail = new PHPMailer(true);
+
+ // Server settings
+ $mail->isSMTP();
+ $mail->Host = comments_config('comments.mail.host');
+ $mail->SMTPAuth = true;
+ $mail->Username = comments_config('comments.mail.username');
+ $mail->Password = comments_config('comments.mail.password');
+ $mail->Port = comments_config('comments.mail.port');
+
+ $encryption = comments_config('comments.mail.encryption');
+ if ($encryption === 'tls') {
+ $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
+ } elseif ($encryption === 'ssl') {
+ $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
+ }
+
+ // Recipients
+ $mail->setFrom(
+ comments_config('comments.mail.from.email'),
+ comments_config('comments.mail.from.name')
+ );
+ $mail->addAddress($email);
+
+ // Content
+ $mail->isHTML(true);
+ $mail->CharSet = 'UTF-8';
+
+ $mail->Subject = i18n('comment_subscribe_confirmation') . ' '.config('blog.title');
+ $mail->Body = "
+ " . i18n('comment_subscribe_thread') . ": ".config('site.url')."
+ " . i18n('comment_subscribe_request') . " ".config('blog.title')."
+ " . i18n('comment_subscribe_never_requested') . "
+ " . i18n('comment_subscribe_click') . " " . i18n('comment_subscribe_here') . " " . i18n('comment_subscribe_confirm_message') . "
+
+ " . i18n('comment_subscribe_unsubscribe_message') . " ".config('blog.title')." " . i18n('comment_subscribe_unsubscribe_anytime') . ": " . i18n('comment_unsubscribe') . " .
+
+ ";
+
+ $mail->send();
+ return true;
+ } catch (Exception $e) {
+ error_log("Subscription notification email failed: {$mail->ErrorInfo}");
+ return false;
+ }
+}
+
+
+
+
+
/**
* Publish a comment (approve from moderation)
*
@@ -479,9 +702,11 @@ function commentPublish($file, $commentId)
if ($comment['id'] === $commentId) {
$comment['published'] = true;
$updated = true;
+
+ $url = get_url_from_file($file);
- // Send notifications to other commenters
- sendCommentNotifications($comment, $comments, false);
+ // Send notifications only to subscribers when publishing (admin already saw it in moderation)
+ sendCommentNotifications($url, $comment, $comments, false, true);
break;
}
}
@@ -491,6 +716,10 @@ function commentPublish($file, $commentId)
}
$json = json_encode($comments, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+
+ rebuilt_cache('all');
+ clear_cache();
+
return file_put_contents($file, $json, LOCK_EX) !== false;
}
@@ -525,6 +754,10 @@ function commentDelete($mdfile, $commentId)
$comments = array_values($comments);
$json = json_encode($comments, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+
+ rebuilt_cache('all');
+ clear_cache();
+
return file_put_contents($file, $json, LOCK_EX) !== false;
}
@@ -587,22 +820,24 @@ function commentModify($file, $commentId, $data)
* @param array $newComment The new comment
* @param array $allComments All comments for this post
* @param bool $notifyAdmin Notify admin (default true)
+ * @param bool $notifySubscribers Notify subscribers (default true)
* @return void
*/
-function sendCommentNotifications($url, $newComment, $allComments, $notifyAdmin = true)
+function sendCommentNotifications($url, $newComment, $allComments, $notifyAdmin = true, $notifySubscribers = true)
{
- // TODO: function to be fixed, still using postId variable
-
- // Check if notifications are enabled
- if (comments_config('comments.notify') !== 'true' ||
- comments_config('comments.mail.enabled') !== 'true') {
+ // Check if mail is enabled
+ if (comments_config('comments.mail.enabled') !== 'true') {
return;
}
$recipients = array();
- // Add admin email
+ // Add admin email - notify if comments.notifyadmin = "true" OR comments.moderation = "true"
if ($notifyAdmin) {
+ $shouldNotifyAdmin = (comments_config('comments.notifyadmin') === 'true') ||
+ (comments_config('comments.moderation') === 'true');
+
+ if ($shouldNotifyAdmin) {
$adminEmail = comments_config('comments.admin.email');
if (!empty($adminEmail) && filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
$recipients[$adminEmail] = array(
@@ -611,16 +846,18 @@ function sendCommentNotifications($url, $newComment, $allComments, $notifyAdmin
);
}
}
-/*
+ }
- // TODO: this part is disabled until a spam-secured way for comment subscription is implemented
-
+ // Add subscribers only if notifySubscribers is true AND comments.notify is enabled
+ if ($notifySubscribers && comments_config('comments.notify') === 'true') {
// Add parent comment author (if replying)
if (!empty($newComment['parent_id'])) {
foreach ($allComments as $comment) {
if ($comment['id'] === $newComment['parent_id'] &&
$comment['notify'] &&
$comment['email'] !== $newComment['email']) {
+ $subscrition = getSubscription($comment['email']);
+ if ($subscrition['status'] == 'subscribed') {
$recipients[$comment['email']] = array(
'name' => $comment['name'],
'type' => 'parent'
@@ -628,14 +865,15 @@ function sendCommentNotifications($url, $newComment, $allComments, $notifyAdmin
}
}
}
+ }
- // Add other commenters in same thread who want notifications
+ // Add all commenters in same thread (same JSON file) who want notifications
foreach ($allComments as $comment) {
if ($comment['notify'] &&
$comment['email'] !== $newComment['email'] &&
$comment['id'] !== $newComment['id']) {
- // Same thread = same parent or no parent
- if ($comment['parent_id'] === $newComment['parent_id']) {
+ $subscrition = getSubscription($comment['email']);
+ if ($subscrition['status'] == 'subscribed') {
$recipients[$comment['email']] = array(
'name' => $comment['name'],
'type' => 'thread'
@@ -643,7 +881,8 @@ function sendCommentNotifications($url, $newComment, $allComments, $notifyAdmin
}
}
}
-*/
+ }
+
// Send emails
foreach ($recipients as $email => $info) {
sendCommentEmail($email, $info['name'], $url, $newComment, $info['type']);
@@ -692,22 +931,30 @@ function sendCommentEmail($to, $toName, $url, $comment, $type = 'admin')
$mail->CharSet = 'UTF-8';
if ($type === 'admin') {
- $mail->Subject = 'New comment awaiting moderation';
+ if (comments_config('comments.moderation') === 'true') {
+ $mail->Subject = i18n('comment_email_admin_awaiting') . " - " . config('blog.title');
+ }
+ else {
+ $mail->Subject = i18n('comment_email_admin_new') . " - " . config('blog.title');
+ }
$mail->Body = "
- New comment on: {$url}
- From: {$comment['name']} ({$comment['email']})
- Comment:
+ ".i18n('comment_email_new').": {$url}
+ " . i18n('comment_email_from') . ": {$comment['name']} ({$comment['email']})
+ " . i18n('comment') . ":
" . nl2br(htmlspecialchars($comment['comment'])) . "
- Moderate comments
+ " . i18n('comment_email_moderate'). "
";
} else {
- $mail->Subject = 'New reply to your comment';
+ $mail->Subject = i18n('comment_email_new_subscribed') . " - " . config('blog.title');
$mail->Body = "
- Someone replied to your comment on: {$url}
- From: {$comment['name']}
- Comment:
+ " . i18n('comment_email_new_replied') .": " . site_url() . "{$url}
+ " . i18n('comment_email_from') . ": {$comment['name']}
+ " . i18n('comment') . ":
" . nl2br(htmlspecialchars($comment['comment'])) . "
- View comment
+ " . i18n('comment_email_view_comment') . "
+
+ " . i18n('comment_subscribe_unsubscribe_message') . " ".config('blog.title')." " . i18n('comment_subscribe_unsubscribe_anytime') . ": " . i18n('comment_unsubscribe') . " .
+
";
}
@@ -764,4 +1011,17 @@ function formatCommentText($text)
return $text;
}
+
+
+
+if (isset($_GET['subscribe'])) {
+ confirmSubscription($_GET['subscribe']);
+}
+
+if (isset($_GET['unsubscribe'])) {
+ deleteSubscription($_GET['unsubscribe']);
+}
+
+
+
?>
\ No newline at end of file
diff --git a/system/includes/comments_readme.md b/system/includes/comments_readme.md
new file mode 100644
index 00000000..6cbf5b19
--- /dev/null
+++ b/system/includes/comments_readme.md
@@ -0,0 +1,28 @@
+# HTMLy comment system
+A commenting system integrated in HTMLy, featuring:
+* threaded comments (comments and replies)
+* antispam (with no external dependencies, no CAPTCHA)
+* notification system and thread subscription
+
+## 2025-12-26
+Some major fixes to comment system:
+* added English strings in notification emails (needs translations in all other languages)
+* improved antispam system
+* added subscription verification system
+
+### Antispam
+Antispam work using a honeyspot and js/token verification
+
+* honeyspot: field "website" is added as hidden - spambot usually fill it, all comments with this field not empty are discarded as SPAM
+* js: javascript must be enabled to have a comment being considered not SPAM - all modern browser have js enabled
+* token: a token with encrypted timestamp is generated and added to "company" hidden field - a comment have to be submitted between 3 and 600 seconds from token generation (this should prevent automated submissions (before 3 seconds) and luckily forged tokens (converting in a number, probably resulting in less than 3 or more than 600 seconds difference)
+
+Both methods can be enabled/disabled from comment system configuration page.
+
+## Subscriptions
+Users can ask for email notification when a new comment is published in a subscribed post thread. A confirmation email is sent to the user email, and subscription must be confirmed clicking on a link. Only confirmed subscription users will receive notification emails.
+Notification email are sent on comment publish (if validation is enabled) or comment insert (if moderation is disabled, not recommended).
+
+**TODO**: limit comment insert by time from same IP address
+
+**TODO**: reworking backend functions to use HTMLy basic functions and avoid code duplication