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