From ff1d1ed8b3ccc64329b5523e83eb75110900f4e9 Mon Sep 17 00:00:00 2001 From: Trevor Forrester Date: Mon, 3 Aug 2026 13:27:35 +1000 Subject: [PATCH 1/4] Start Jacaranda Comments Advanced 01.02.00 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create the Advanced release line from the stable Jacaranda Comments 01.01.03 code base and add portal-wide administration. Changes: Add a portal-scoped Site-wide Jacaranda Comments Settings panel Add portal-wide emergency controls for all posting and guest posting Add central defaults for moderation, guest access, language filtering, comment length, rate limiting, CAPTCHA and email notifications Add explicit per-module inheritance of portal defaults Keep inheritance disabled by default so existing module instances retain their current local settings Restrict the central panel to DNN Superusers and members of the portal’s built-in Administrators role Create no additional DNN security roles Add server-side portal isolation, permission checks and anti-CSRF validation Use parameterised SQL for all central-settings database operations Add audit details recording who changed the central settings and when Add the portal-scoped JacarandaCommentsPortalSettings database table Preserve existing comments, replies and module settings Preserve guest commenting, registered-user editing, moderation, language filtering, CAPTCHA, rate limiting, email notifications and accessibility improvements Rename the unpublished 01.01.04 development work to Advanced 01.02.00 Update the manifest, SQL upgrade script, release notes, documentation, resources and package files The stable Simple edition remains on the main branch at version 01.01.03. This Advanced edition retains the existing DNN package identity and is an alternative upgrade path rather than a separate side-by-side module. --- Jacaranda_Comments.dnn | 20 +- .../SqlDataProvider/01.02.00.SqlDataProvider | 34 + .../SqlDataProvider/Uninstall.SqlDataProvider | 11 + README_01.02.00.txt | 86 +++ ReleaseNotes.txt | 16 +- Resources.zip | Bin 24922 -> 34758 bytes Resources/PortalSettings.ascx | 663 ++++++++++++++++++ Resources/Settings.ascx | 166 ++++- Resources/View.ascx | 385 +++++++++- Resources/module.css | 71 +- 10 files changed, 1421 insertions(+), 31 deletions(-) create mode 100644 Providers/DataProviders/SqlDataProvider/01.02.00.SqlDataProvider create mode 100644 README_01.02.00.txt create mode 100644 Resources/PortalSettings.ascx diff --git a/Jacaranda_Comments.dnn b/Jacaranda_Comments.dnn index 51fbbb9..e8f24ae 100644 --- a/Jacaranda_Comments.dnn +++ b/Jacaranda_Comments.dnn @@ -1,9 +1,9 @@ - + - + Jacaranda Comments - Page-level comments and replies module for DNN 10 with opt-in moderated guest posting, registered-author editing, a private per-module language filter, configurable comment length, page-aware moderator email subjects, module-aware accessible notifications, a prominent accessible CAPTCHA answer field, rate limiting, email notifications, and optional CAPTCHA. + Advanced page-level comments and replies module for DNN 10 with portal-wide defaults and emergency posting controls, opt-in moderated guest posting, registered-author editing, a private language filter, configurable comment length, page-aware moderator email subjects, accessible module-aware notifications, rate limiting, email notifications, and optional CAPTCHA. Trevor Forrester Forrest It Services @@ -135,6 +135,11 @@ 01.01.03.SqlDataProvider 01.01.03 + + +
+ + Site-wide Jacaranda Comments settings are restricted to DNN portal administrators and superusers. + + + +

Site-wide Jacaranda Comments Settings

+

+ These settings apply only to this DNN portal. Emergency switches affect every Jacaranda Comments instance in the portal. Defaults affect only modules that explicitly choose to inherit site-wide settings. +

+ + + + + + + +
+ Emergency controls + +
+ +

+ Clearing this switch immediately disables all new comments and replies across the portal. Existing comments remain visible and moderators can still approve or delete them. +

+
+ +
+ +

+ Clearing this switch disables guest posting across every module, even where a local module setting or inherited default would otherwise allow guests. Registered-user posting is unaffected. +

+
+
+ +
+ Portal defaults +

+ These values are used only by module instances with “Use site-wide defaults” enabled. Existing modules keep their current local settings after upgrade until an administrator deliberately opts them in. +

+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +

Enter one private term or phrase per line. Maximum 250 entries and 100 characters per entry.

+
+ +
+ + + + +
+
+ +
+ Default rate limiting +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ Default CAPTCHA +
+ +
+
+ +
+ Default email notifications +
+ +
+
+ + +

Separate multiple addresses with commas or semicolons. Leave blank to use the portal email address.

+
+
+ +
+
+ +
+ +
+ + +
+
+
diff --git a/Resources/Settings.ascx b/Resources/Settings.ascx index 7d247a3..6e83987 100644 --- a/Resources/Settings.ascx +++ b/Resources/Settings.ascx @@ -1,6 +1,10 @@ <%@ Control Language="C#" AutoEventWireup="true" Inherits="DotNetNuke.Entities.Modules.ModuleSettingsBase" %> <%@ Import Namespace="System" %> <%@ Import Namespace="System.Collections.Generic" %> +<%@ Import Namespace="System.Data" %> +<%@ Import Namespace="System.Data.SqlClient" %> +<%@ Import Namespace="DotNetNuke.Common.Utilities" %> +<%@ Import Namespace="DotNetNuke.Data" %> <%@ Import Namespace="DotNetNuke.Entities.Modules" %>

Jacaranda Comments Settings

+
+ Central settings + +
+ +

+ Default: off. Existing modules continue using their current local settings after upgrade. When enabled, the local values below remain stored but are ignored until this option is switched off again. +

+
+ +

+

+ + + +

This administrator-only panel controls portal defaults and emergency switches for every Jacaranda Comments instance in this portal.

+
+
+
Guest commenting @@ -372,6 +536,6 @@

- Settings are stored as DNN module settings, so each instance can independently allow or block guest posting and use its own comment length, moderation, notification, and anti-spam behaviour. + Local values remain stored as DNN module settings. Each instance can either keep those values or deliberately inherit the portal-wide defaults. Portal emergency switches always take priority.

diff --git a/Resources/View.ascx b/Resources/View.ascx index cb9e78a..29cc8ed 100644 --- a/Resources/View.ascx +++ b/Resources/View.ascx @@ -1,4 +1,4 @@ -<%@ Control Language="C#" AutoEventWireup="true" Inherits="DotNetNuke.Entities.Modules.PortalModuleBase" %> +<%@ Control Language="C#" AutoEventWireup="true" Inherits="DotNetNuke.Entities.Modules.PortalModuleBase" %> <%@ Import Namespace="System" %> <%@ Import Namespace="System.Collections.Generic" %> <%@ Import Namespace="System.Data" %> @@ -37,6 +37,9 @@ private const string PostRedirectModuleQueryKey = "jcmid"; private const string PostRedirectCommentQueryKey = "jccid"; + private PortalCommentSettings _portalCommentSettings; + private bool _portalCommentSettingsLoaded; + private string PostRedirectMessageAnchorId { get { return PostRedirectMessageAnchorPrefix + ModuleId; } @@ -54,6 +57,18 @@ } } + private string PortalSettingsTable + { + get + { + var provider = DataProvider.Instance(); + var owner = CleanSqlIdentifierPart(provider.DatabaseOwner, "dbo"); + var qualifier = CleanSqlIdentifierPart(provider.ObjectQualifier, String.Empty); + + return "[" + owner + "].[" + qualifier + "JacarandaCommentsPortalSettings]"; + } + } + private string SecurityTokenSessionKey { get @@ -96,14 +111,42 @@ get { return UserInfo != null && UserId > -1 && !UserInfo.IsDeleted; } } + private bool UsePortalSettings + { + get { return GetModuleSettingBool("UsePortalSettings", false); } + } + + private bool PortalPostingEnabled + { + get + { + var settings = GetPortalCommentSettings(); + return !settings.Available || settings.PostingEnabled; + } + } + + private bool PortalGuestPostingEnabled + { + get + { + var settings = GetPortalCommentSettings(); + return !settings.Available || settings.GuestPostingEnabled; + } + } + private bool AllowGuestComments { - get { return GetModuleSettingBool("AllowGuestComments", false); } + get + { + var localValue = GetModuleSettingBool("AllowGuestComments", false); + var effectiveValue = GetEffectivePortalBool(localValue, delegate(PortalCommentSettings value) { return value.DefaultAllowGuestComments; }); + return PortalGuestPostingEnabled && effectiveValue; + } } protected bool CanPostComments { - get { return IsRegisteredCommentUser || AllowGuestComments; } + get { return PortalPostingEnabled && (IsRegisteredCommentUser || AllowGuestComments); } } private bool IsGuestPoster @@ -116,71 +159,142 @@ return UserInfo != null && (UserInfo.IsSuperUser || IsEditable); } + private bool CanManagePortalCommentSettings() + { + if (UserInfo == null) + { + return false; + } + + if (UserInfo.IsSuperUser) + { + return true; + } + + var administratorRoleName = PortalSettings == null + ? String.Empty + : (PortalSettings.AdministratorRoleName ?? String.Empty).Trim(); + + return !String.IsNullOrWhiteSpace(administratorRoleName) + && UserInfo.IsInRole(administratorRoleName); + } + private bool RequireApprovalForNonEditors { - get { return GetModuleSettingBool("RequireApprovalForNonEditors", true); } + get + { + var localValue = GetModuleSettingBool("RequireApprovalForNonEditors", true); + return GetEffectivePortalBool(localValue, delegate(PortalCommentSettings value) { return value.DefaultRequireApprovalForNonEditors; }); + } } private bool EnableLanguageFilter { - get { return GetModuleSettingBool("EnableLanguageFilter", false); } + get + { + var localValue = GetModuleSettingBool("EnableLanguageFilter", false); + return GetEffectivePortalBool(localValue, delegate(PortalCommentSettings value) { return value.DefaultEnableLanguageFilter; }); + } } private string BlockedLanguageTerms { - get { return GetModuleSettingString("BlockedLanguageTerms", String.Empty); } + get + { + var localValue = GetModuleSettingString("BlockedLanguageTerms", String.Empty); + return GetEffectivePortalString(localValue, delegate(PortalCommentSettings value) { return value.DefaultBlockedLanguageTerms; }); + } } private int MaximumCommentLength { get { - return GetModuleSettingInt( + var localValue = GetModuleSettingInt( "MaximumCommentLength", DefaultMaximumCommentLength, MinimumMaximumCommentLength, MaximumMaximumCommentLength); + + return GetEffectivePortalInt( + localValue, + delegate(PortalCommentSettings value) { return value.DefaultMaximumCommentLength; }, + DefaultMaximumCommentLength, + MinimumMaximumCommentLength, + MaximumMaximumCommentLength); } } private bool EnableRateLimiting { - get { return GetModuleSettingBool("EnableRateLimiting", true); } + get + { + var localValue = GetModuleSettingBool("EnableRateLimiting", true); + return GetEffectivePortalBool(localValue, delegate(PortalCommentSettings value) { return value.DefaultEnableRateLimiting; }); + } } private int RateLimitSeconds { - get { return GetModuleSettingInt("RateLimitSeconds", 60, 0, 3600); } + get + { + var localValue = GetModuleSettingInt("RateLimitSeconds", 60, 0, 3600); + return GetEffectivePortalInt(localValue, delegate(PortalCommentSettings value) { return value.DefaultRateLimitSeconds; }, 60, 0, 3600); + } } private int RateLimitMaxPosts { - get { return GetModuleSettingInt("RateLimitMaxPosts", 5, 1, 100); } + get + { + var localValue = GetModuleSettingInt("RateLimitMaxPosts", 5, 1, 100); + return GetEffectivePortalInt(localValue, delegate(PortalCommentSettings value) { return value.DefaultRateLimitMaxPosts; }, 5, 1, 100); + } } private int RateLimitWindowMinutes { - get { return GetModuleSettingInt("RateLimitWindowMinutes", 15, 1, 1440); } + get + { + var localValue = GetModuleSettingInt("RateLimitWindowMinutes", 15, 1, 1440); + return GetEffectivePortalInt(localValue, delegate(PortalCommentSettings value) { return value.DefaultRateLimitWindowMinutes; }, 15, 1, 1440); + } } private bool EnableCaptcha { - get { return GetModuleSettingBool("EnableCaptcha", false); } + get + { + var localValue = GetModuleSettingBool("EnableCaptcha", false); + return GetEffectivePortalBool(localValue, delegate(PortalCommentSettings value) { return value.DefaultEnableCaptcha; }); + } } private bool EnableNotifications { - get { return GetModuleSettingBool("EnableNotifications", false); } + get + { + var localValue = GetModuleSettingBool("EnableNotifications", false); + return GetEffectivePortalBool(localValue, delegate(PortalCommentSettings value) { return value.DefaultEnableNotifications; }); + } } private string NotificationEmailAddresses { - get { return GetModuleSettingString("NotificationEmailAddresses", String.Empty); } + get + { + var localValue = GetModuleSettingString("NotificationEmailAddresses", String.Empty); + return GetEffectivePortalString(localValue, delegate(PortalCommentSettings value) { return value.DefaultNotificationEmailAddresses; }); + } } private bool IncludeCommentTextInNotifications { - get { return GetModuleSettingBool("IncludeCommentTextInNotifications", true); } + get + { + var localValue = GetModuleSettingBool("IncludeCommentTextInNotifications", true); + return GetEffectivePortalBool(localValue, delegate(PortalCommentSettings value) { return value.DefaultIncludeCommentTextInNotifications; }); + } } private bool CaptchaAppliesToCurrentUser @@ -296,6 +410,12 @@ pnlCommentForm.Visible = CanPostComments; pnlLoginRequired.Visible = !CanPostComments; + litPostingUnavailable.Text = Server.HtmlEncode(GetPostingUnavailableMessage()); + lnkPortalSettings.Visible = CanManagePortalCommentSettings(); + if (lnkPortalSettings.Visible) + { + lnkPortalSettings.NavigateUrl = EditUrl("PortalSettings"); + } pnlGuestEmail.Visible = isGuest; pnlGuestNotice.Visible = isGuest; pnlCaptcha.Visible = CaptchaAppliesToCurrentUser; @@ -438,7 +558,7 @@ ORDER BY CreatedOnDate ASC;"; if (!CanPostComments) { - ShowMessage("Please sign in before posting a comment.", false); + ShowMessage(GetPostingUnavailableMessage(), false); ConfigureForm(); BindComments(); return; @@ -847,6 +967,14 @@ SELECT CONVERT(INT, SCOPE_IDENTITY());"; if (String.Equals(e.CommandName, "EditComment", StringComparison.OrdinalIgnoreCase)) { + if (!PortalPostingEnabled) + { + ShowMessage(GetPostingUnavailableMessage(), false); + ConfigureForm(); + BindComments(); + return; + } + if (!IsRegisteredCommentUser) { ShowMessage("Guest comments cannot be edited. Register or sign in before posting to receive the 15-minute edit window.", false); @@ -883,7 +1011,7 @@ SELECT CONVERT(INT, SCOPE_IDENTITY());"; { if (!CanPostComments) { - ShowMessage("Please sign in before replying, or enable guest commenting in this module's settings.", false); + ShowMessage(GetPostingUnavailableMessage(), false); ConfigureForm(); BindComments(); return; @@ -964,7 +1092,7 @@ SELECT CONVERT(INT, SCOPE_IDENTITY());"; isReply = false; errorMessage = "That comment cannot be edited."; - if (!IsRegisteredCommentUser || commentId <= 0) + if (!PortalPostingEnabled || !IsRegisteredCommentUser || commentId <= 0) { return false; } @@ -1734,8 +1862,11 @@ WHERE PortalId = @PortalId return; } - var subjectPrefix = isEdit ? "Edited " : (isGuest ? "New guest " : "New "); - var subject = CleanEmailHeader(subjectPrefix + (isReply ? "reply" : "comment") + " on " + GetPortalName()); + var pageTitle = GetPageTitle(); + var subjectAction = !isApproved + ? "Comment awaiting approval" + : (isEdit ? "Comment edited" : "New comment"); + var subject = CleanEmailHeader(subjectAction + " — " + pageTitle); var body = BuildNotificationBody( commentId, isReply, @@ -1900,7 +2031,8 @@ WHERE PortalId = @PortalId + (isEdit ? " has been edited." : " has been submitted.")); body.AppendLine(); body.AppendLine("Portal: " + EncodeForNotification(GetPortalName())); - body.AppendLine("Page: " + EncodeForNotification(GetPageUrl())); + body.AppendLine("Page title: " + EncodeForNotification(GetPageTitle())); + body.AppendLine("Page link: " + EncodeForNotification(GetPageUrl())); body.AppendLine("Module: " + EncodeForNotification(ModuleConfiguration != null ? ModuleConfiguration.ModuleTitle : "Jacaranda Comments")); body.AppendLine("Comment ID: " + commentId); body.AppendLine("Author: " + EncodeForNotification(displayName)); @@ -1948,6 +2080,28 @@ WHERE PortalId = @PortalId return "DNN site"; } + private string GetPageTitle() + { + try + { + if (PortalSettings != null && PortalSettings.ActiveTab != null) + { + var tabName = (PortalSettings.ActiveTab.TabName ?? String.Empty).Trim(); + + if (!String.IsNullOrWhiteSpace(tabName)) + { + return tabName; + } + } + } + catch + { + // Fall through to a safe, non-empty subject value. + } + + return "DNN page"; + } + private string GetPageUrl() { try @@ -2055,6 +2209,180 @@ WHERE CommentId = @CommentId } } + private string GetPostingUnavailableMessage() + { + if (!PortalPostingEnabled) + { + return "New comments and replies are temporarily disabled across this site. Existing comments remain available to read."; + } + + if (!IsRegisteredCommentUser && !AllowGuestComments) + { + return "Please sign in to leave a comment or reply. Guest commenting is currently switched off for this module or portal."; + } + + return "Comment posting is currently unavailable."; + } + + private bool GetEffectivePortalBool(bool localValue, Func selector) + { + var settings = GetPortalCommentSettings(); + return UsePortalSettings && settings.Available ? selector(settings) : localValue; + } + + private string GetEffectivePortalString(string localValue, Func selector) + { + var settings = GetPortalCommentSettings(); + return UsePortalSettings && settings.Available ? (selector(settings) ?? String.Empty) : localValue; + } + + private int GetEffectivePortalInt( + int localValue, + Func selector, + int defaultValue, + int minimumValue, + int maximumValue) + { + var settings = GetPortalCommentSettings(); + var value = UsePortalSettings && settings.Available ? selector(settings) : localValue; + + if (value < minimumValue) value = minimumValue; + if (value > maximumValue) value = maximumValue; + return value; + } + + private PortalCommentSettings GetPortalCommentSettings() + { + if (_portalCommentSettingsLoaded) + { + return _portalCommentSettings; + } + + _portalCommentSettingsLoaded = true; + _portalCommentSettings = PortalCommentSettings.CreateDefaults(); + + try + { + using (var connection = new SqlConnection(ConnectionString)) + using (var command = connection.CreateCommand()) + { + command.CommandText = @" +SELECT PostingEnabled, + GuestPostingEnabled, + DefaultAllowGuestComments, + DefaultRequireApprovalForNonEditors, + DefaultEnableLanguageFilter, + DefaultBlockedLanguageTerms, + DefaultMaximumCommentLength, + DefaultEnableRateLimiting, + DefaultRateLimitSeconds, + DefaultRateLimitMaxPosts, + DefaultRateLimitWindowMinutes, + DefaultEnableCaptcha, + DefaultEnableNotifications, + DefaultNotificationEmailAddresses, + DefaultIncludeCommentTextInNotifications +FROM " + PortalSettingsTable + @" +WHERE PortalId = @PortalId;"; + command.Parameters.Add("@PortalId", SqlDbType.Int).Value = PortalId; + connection.Open(); + _portalCommentSettings.Available = true; + + using (var reader = command.ExecuteReader(CommandBehavior.SingleRow)) + { + if (!reader.Read()) + { + return _portalCommentSettings; + } + + _portalCommentSettings.PostingEnabled = ReadPortalBool(reader, "PostingEnabled", true); + _portalCommentSettings.GuestPostingEnabled = ReadPortalBool(reader, "GuestPostingEnabled", true); + _portalCommentSettings.DefaultAllowGuestComments = ReadPortalBool(reader, "DefaultAllowGuestComments", false); + _portalCommentSettings.DefaultRequireApprovalForNonEditors = ReadPortalBool(reader, "DefaultRequireApprovalForNonEditors", true); + _portalCommentSettings.DefaultEnableLanguageFilter = ReadPortalBool(reader, "DefaultEnableLanguageFilter", false); + _portalCommentSettings.DefaultBlockedLanguageTerms = ReadPortalString(reader, "DefaultBlockedLanguageTerms", String.Empty); + _portalCommentSettings.DefaultMaximumCommentLength = ReadPortalInt(reader, "DefaultMaximumCommentLength", 4000); + _portalCommentSettings.DefaultEnableRateLimiting = ReadPortalBool(reader, "DefaultEnableRateLimiting", true); + _portalCommentSettings.DefaultRateLimitSeconds = ReadPortalInt(reader, "DefaultRateLimitSeconds", 60); + _portalCommentSettings.DefaultRateLimitMaxPosts = ReadPortalInt(reader, "DefaultRateLimitMaxPosts", 5); + _portalCommentSettings.DefaultRateLimitWindowMinutes = ReadPortalInt(reader, "DefaultRateLimitWindowMinutes", 15); + _portalCommentSettings.DefaultEnableCaptcha = ReadPortalBool(reader, "DefaultEnableCaptcha", false); + _portalCommentSettings.DefaultEnableNotifications = ReadPortalBool(reader, "DefaultEnableNotifications", false); + _portalCommentSettings.DefaultNotificationEmailAddresses = ReadPortalString(reader, "DefaultNotificationEmailAddresses", String.Empty); + _portalCommentSettings.DefaultIncludeCommentTextInNotifications = ReadPortalBool(reader, "DefaultIncludeCommentTextInNotifications", true); + } + } + } + catch + { + _portalCommentSettings = PortalCommentSettings.CreateDefaults(); + _portalCommentSettings.Available = false; + } + + return _portalCommentSettings; + } + + private static bool ReadPortalBool(IDataRecord record, string name, bool defaultValue) + { + var ordinal = record.GetOrdinal(name); + return record.IsDBNull(ordinal) ? defaultValue : Convert.ToBoolean(record.GetValue(ordinal)); + } + + private static int ReadPortalInt(IDataRecord record, string name, int defaultValue) + { + var ordinal = record.GetOrdinal(name); + return record.IsDBNull(ordinal) ? defaultValue : Convert.ToInt32(record.GetValue(ordinal)); + } + + private static string ReadPortalString(IDataRecord record, string name, string defaultValue) + { + var ordinal = record.GetOrdinal(name); + return record.IsDBNull(ordinal) ? defaultValue : Convert.ToString(record.GetValue(ordinal)); + } + + private sealed class PortalCommentSettings + { + public bool Available { get; set; } + public bool PostingEnabled { get; set; } + public bool GuestPostingEnabled { get; set; } + public bool DefaultAllowGuestComments { get; set; } + public bool DefaultRequireApprovalForNonEditors { get; set; } + public bool DefaultEnableLanguageFilter { get; set; } + public string DefaultBlockedLanguageTerms { get; set; } + public int DefaultMaximumCommentLength { get; set; } + public bool DefaultEnableRateLimiting { get; set; } + public int DefaultRateLimitSeconds { get; set; } + public int DefaultRateLimitMaxPosts { get; set; } + public int DefaultRateLimitWindowMinutes { get; set; } + public bool DefaultEnableCaptcha { get; set; } + public bool DefaultEnableNotifications { get; set; } + public string DefaultNotificationEmailAddresses { get; set; } + public bool DefaultIncludeCommentTextInNotifications { get; set; } + + public static PortalCommentSettings CreateDefaults() + { + return new PortalCommentSettings + { + Available = false, + PostingEnabled = true, + GuestPostingEnabled = true, + DefaultAllowGuestComments = false, + DefaultRequireApprovalForNonEditors = true, + DefaultEnableLanguageFilter = false, + DefaultBlockedLanguageTerms = String.Empty, + DefaultMaximumCommentLength = 4000, + DefaultEnableRateLimiting = true, + DefaultRateLimitSeconds = 60, + DefaultRateLimitMaxPosts = 5, + DefaultRateLimitWindowMinutes = 15, + DefaultEnableCaptcha = false, + DefaultEnableNotifications = false, + DefaultNotificationEmailAddresses = String.Empty, + DefaultIncludeCommentTextInNotifications = true + }; + } + } + private string GetModuleSettingString(string name, string defaultValue) { var key = SettingPrefix + name; @@ -2173,7 +2501,7 @@ WHERE CommentId = @CommentId protected bool CanEditComment(object commentUserId, object createdOnDate) { - if (!IsRegisteredCommentUser || commentUserId == null || commentUserId == DBNull.Value + if (!PortalPostingEnabled || !IsRegisteredCommentUser || commentUserId == null || commentUserId == DBNull.Value || createdOnDate == null || createdOnDate == DBNull.Value) { return false; @@ -2952,8 +3280,15 @@ WHERE CommentId = @CommentId
-

Comments

- +
+

Comments

+ +
+
- Please sign in to leave a comment or reply. Guest commenting is currently switched off for this module. + diff --git a/Resources/module.css b/Resources/module.css index a0c973d..7afdf16 100644 --- a/Resources/module.css +++ b/Resources/module.css @@ -1,4 +1,4 @@ -/* Jacaranda Comments module +/* Jacaranda Comments module Scoped so it does not interfere with DNN edit-mode overlays or the Jacaranda navbar. */ .jacaranda-comments { @@ -598,3 +598,72 @@ .jacaranda-comments .jc-language-terms { min-height: 9rem; } + +/* 01.02.00 Advanced portal-wide settings and administrator navigation */ +.jacaranda-comments .jc-header-title { + align-items: baseline; + display: flex; + flex-wrap: wrap; + gap: 0.65rem; +} + +.jacaranda-comments .jc-admin-settings, +.jacaranda-comments .jc-secondary-button { + background: #f5f5f5; + border: 2px solid #333; + border-radius: 4px; + color: #111; + display: inline-block; + font-weight: 700; + line-height: 1.25; + padding: 0.48rem 0.7rem; + text-decoration: none; +} + +.jacaranda-comments .jc-admin-settings:hover, +.jacaranda-comments .jc-admin-settings:focus, +.jacaranda-comments .jc-secondary-button:hover, +.jacaranda-comments .jc-secondary-button:focus { + background: #fff; + border-color: #005fcc; + color: #003f88; + outline: 3px solid rgba(0, 95, 204, 0.25); + outline-offset: 2px; +} + +.jacaranda-comments .jc-inheritance-status, +.jacaranda-comments .jc-portal-emergency-status, +.jacaranda-comments .jc-audit-note { + background: #f5f8fb; + border-left: 4px solid #315f86; + color: #1c3448; + margin: 0.65rem 0; + padding: 0.65rem 0.8rem; + text-indent: 0; +} + +.jacaranda-comments .jc-portal-emergency-status { + border-left-color: #8a5a00; +} + +.jacaranda-comments .jc-central-settings-link { + margin-top: 0.85rem; +} + +.jacaranda-comments .jc-emergency-settings { + border: 2px solid #8a5a00; +} + +.jacaranda-comments .jc-settings-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1rem; +} + +@media (max-width: 767.98px) { + .jacaranda-comments .jc-admin-settings { + width: 100%; + } +} From 06f5b154f1fce6096ee318fb5ef8ebe036c74695 Mon Sep 17 00:00:00 2001 From: Trevor Forrester Date: Sun, 16 Aug 2026 15:43:34 +1000 Subject: [PATCH 2/4] Release Jacaranda Comments Advanced 01.02.02 Add a secure five-minute correction window for guest comments and replies while they are still awaiting moderation. Changes: Allow guests to edit the text of their own pending comment or reply for up to five minutes after the original submission Keep the five-minute window tied to the original posting time so editing does not restart the timer End guest editing immediately when a submission is approved, deleted, guest posting is disabled, or all posting is disabled Restrict guest editing to comment/reply text only; guest display name and private email remain unchanged Generate a cryptographically random guest-edit credential and store only its SHA-256 hash with the comment Keep the raw guest-edit credential out of the database, URLs, query strings, hidden fields, rendered HTML, and moderator emails Do not use guest name, email, IP address, user-agent, rate-limit key, or Comment ID alone as proof of ownership Revalidate PortalId, TabId, ModuleId, guest ownership, moderation status, deletion status, token hash, and five-minute expiry during the database update Keep corrected guest submissions pending moderation Re-run the private language filter after a guest correction Preserve the existing 15-minute editing window for registered users Preserve portal-wide moderation, emergency switches, CAPTCHA, rate limiting, language filtering, email notifications, approval, deletion, and security-token validation Add GuestEditTokenHash to support secure guest correction Add and register the 01.02.02 SqlDataProvider upgrade script Update the manifest, release notes, documentation, resources, and package files Existing guest comments are not made editable by this upgrade. Only new guest submissions created after 01.02.02 receive the secure correction capability. This release is part of the Jacaranda Comments Advanced branch. The stable Simple edition remains separate on the main branch. --- Jacaranda_Comments.dnn | 16 +- .../SqlDataProvider/01.02.02.SqlDataProvider | 18 + README_01.02.02.txt | 84 +++ ReleaseNotes.txt | 27 + Resources.zip | Bin 34758 -> 39952 bytes Resources/PortalSettings.ascx | 417 +++++++++++++- Resources/Settings.ascx | 6 +- Resources/View.ascx | 511 +++++++++++++++--- 8 files changed, 991 insertions(+), 88 deletions(-) create mode 100644 Providers/DataProviders/SqlDataProvider/01.02.02.SqlDataProvider create mode 100644 README_01.02.02.txt diff --git a/Jacaranda_Comments.dnn b/Jacaranda_Comments.dnn index e8f24ae..c558c09 100644 --- a/Jacaranda_Comments.dnn +++ b/Jacaranda_Comments.dnn @@ -1,9 +1,9 @@ - + Jacaranda Comments - Advanced page-level comments and replies module for DNN 10 with portal-wide defaults and emergency posting controls, opt-in moderated guest posting, registered-author editing, a private language filter, configurable comment length, page-aware moderator email subjects, accessible module-aware notifications, rate limiting, email notifications, and optional CAPTCHA. + Advanced page-level comments and replies module for DNN 10 with portal-wide moderation, portal-wide defaults and emergency posting controls, opt-in moderated guest posting with a secure five-minute correction window, registered-author editing, a private language filter, configurable comment length, page-aware moderator email subjects, accessible module-aware notifications, rate limiting, email notifications, and optional CAPTCHA. Trevor Forrester Forrest It Services @@ -140,6 +140,16 @@ 01.02.00.SqlDataProvider 01.02.00 + + +
-

Jacaranda Comments Settings

+

Jacaranda Comments Advanced Settings

- Central settings + Central administration -
- -

- Default: off. Existing modules continue using their current local settings after upgrade. When enabled, the local values below remain stored but are ignored until this option is switched off again. -

-
+

+ Jacaranda Comments Advanced 01.03.00 manages its commenting configuration in one place for the whole DNN portal. Page-level Jacaranda settings are no longer edited here. +

+ +

-

-

+

+ Existing local Jacaranda module settings are retained in DNN for migration safety, but after central configuration is activated they are ignored by the Advanced edition. DNN's normal module title, container, visibility and permission settings remain managed by DNN itself. +

-

This administrator-only panel provides portal-wide pending moderation, defaults, and emergency switches for every Jacaranda Comments instance in this portal.

-
-
- -
- Guest commenting - -
- -

- Default: off. Guest name and email are required, the email is never shown publicly, and every guest submission is held for approval. Guests may correct only their submitted text for up to 5 minutes while it remains awaiting approval; registered authors receive the 15-minute edit window. -

-

- For public guest commenting, enable CAPTCHA and keep rate limiting enabled. You can turn guest commenting off immediately without affecting existing comments. -

-
-
- -
- Moderation - -
-

- Editors, administrators, and superusers can still post immediately and approve/delete comments from the module view. + Review portal-wide settings, activate central configuration, moderate pending comments, and manage emergency posting controls here.

-
-
- -
- Language filter - -
- -

- Default: off. A matching submission is kept unchanged but forced into moderation. The visitor is only told that the submission is waiting for approval. -

-
+ -
- - -

- Enter one term or phrase per line. Matching is not case-sensitive and treats punctuation as a separator. The list is available only on this authorised settings screen and is not sent to the public comment form. Up to 250 entries are retained, with a maximum of 100 characters per entry. -

+

- This is a moderation aid, not a complete content-safety system. Review flagged submissions before approval because simple language filters can produce false matches or be deliberately evaded. -

-
-
- -
- Comment length - -
- - - - -

- Default: 4,000 characters. This setting applies to both comments and replies and is stored separately for each module instance. -

-
-
- -
- Rate limiting - -
- -
- -
-
- - -
- -
- - -
- -
- - -
-
- -

- Default: 1 minute between posts and 5 posts per 15 minutes. Moderators are not rate-limited. -

-
- -
- CAPTCHA - -
- -

- This avoids third-party scripts and keys. It applies to registered non-editors and to guests when guest commenting is enabled. + Jacaranda Comments Advanced configuration can be changed only by a DNN portal Administrator or Superuser through Comments Administration.

-
-
- -
- Email notifications - -
- -
- -
- - -

- Separate multiple addresses with commas or semicolons. Leave blank to use the portal email address. -

-
- -
- -
+
- -

- Local values remain stored as DNN module settings. Each instance can either keep those values or deliberately inherit the portal-wide defaults. Portal emergency switches always take priority. -

diff --git a/Resources/View.ascx b/Resources/View.ascx index 65d3998..6ccb71e 100644 --- a/Resources/View.ascx +++ b/Resources/View.ascx @@ -121,10 +121,6 @@ get { return UserInfo != null && UserId > -1 && !UserInfo.IsDeleted; } } - private bool UsePortalSettings - { - get { return GetModuleSettingBool("UsePortalSettings", false); } - } private bool PortalPostingEnabled { @@ -2521,7 +2517,7 @@ WHERE CommentId = @CommentId if (!IsRegisteredCommentUser && !AllowGuestComments) { - return "Please sign in to leave a comment or reply. Guest commenting is currently switched off for this module or portal."; + return "Please sign in to leave a comment or reply. Guest commenting is currently switched off."; } return "Comment posting is currently unavailable."; @@ -2530,13 +2526,13 @@ WHERE CommentId = @CommentId private bool GetEffectivePortalBool(bool localValue, Func selector) { var settings = GetPortalCommentSettings(); - return UsePortalSettings && settings.Available ? selector(settings) : localValue; + return settings.Available && settings.CentralSettingsActive ? selector(settings) : localValue; } private string GetEffectivePortalString(string localValue, Func selector) { var settings = GetPortalCommentSettings(); - return UsePortalSettings && settings.Available ? (selector(settings) ?? String.Empty) : localValue; + return settings.Available && settings.CentralSettingsActive ? (selector(settings) ?? String.Empty) : localValue; } private int GetEffectivePortalInt( @@ -2547,7 +2543,7 @@ WHERE CommentId = @CommentId int maximumValue) { var settings = GetPortalCommentSettings(); - var value = UsePortalSettings && settings.Available ? selector(settings) : localValue; + var value = settings.Available && settings.CentralSettingsActive ? selector(settings) : localValue; if (value < minimumValue) value = minimumValue; if (value > maximumValue) value = maximumValue; @@ -2572,6 +2568,7 @@ WHERE CommentId = @CommentId command.CommandText = @" SELECT PostingEnabled, GuestPostingEnabled, + CentralSettingsActive, DefaultAllowGuestComments, DefaultRequireApprovalForNonEditors, DefaultEnableLanguageFilter, @@ -2600,6 +2597,7 @@ WHERE PortalId = @PortalId;"; _portalCommentSettings.PostingEnabled = ReadPortalBool(reader, "PostingEnabled", true); _portalCommentSettings.GuestPostingEnabled = ReadPortalBool(reader, "GuestPostingEnabled", true); + _portalCommentSettings.CentralSettingsActive = ReadPortalBool(reader, "CentralSettingsActive", false); _portalCommentSettings.DefaultAllowGuestComments = ReadPortalBool(reader, "DefaultAllowGuestComments", false); _portalCommentSettings.DefaultRequireApprovalForNonEditors = ReadPortalBool(reader, "DefaultRequireApprovalForNonEditors", true); _portalCommentSettings.DefaultEnableLanguageFilter = ReadPortalBool(reader, "DefaultEnableLanguageFilter", false); @@ -2648,6 +2646,7 @@ WHERE PortalId = @PortalId;"; public bool Available { get; set; } public bool PostingEnabled { get; set; } public bool GuestPostingEnabled { get; set; } + public bool CentralSettingsActive { get; set; } public bool DefaultAllowGuestComments { get; set; } public bool DefaultRequireApprovalForNonEditors { get; set; } public bool DefaultEnableLanguageFilter { get; set; } @@ -2669,6 +2668,7 @@ WHERE PortalId = @PortalId;"; Available = false, PostingEnabled = true, GuestPostingEnabled = true, + CentralSettingsActive = false, DefaultAllowGuestComments = false, DefaultRequireApprovalForNonEditors = true, DefaultEnableLanguageFilter = false,