diff --git a/source/DasBlog All.sln b/source/DasBlog All.sln index 3e701f6f..d9cb6790 100644 --- a/source/DasBlog All.sln +++ b/source/DasBlog All.sln @@ -28,6 +28,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DasBlog.Test.Integration", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DasBlog.CLI", "DasBlog.CLI\DasBlog.CLI.csproj", "{3DEF6C9E-293A-4E41-9CEC-3466E3EEC754}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Subtext.Akismet", "Subtext.Akismet\Subtext.Akismet.csproj", "{3937987F-789E-4AB9-BAC8-8773ADCC5ED9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug - All|Any CPU = Debug - All|Any CPU diff --git a/source/DasBlog.Services/ConfigFile/SiteConfig.cs b/source/DasBlog.Services/ConfigFile/SiteConfig.cs index aebf26be..2efdc26a 100644 --- a/source/DasBlog.Services/ConfigFile/SiteConfig.cs +++ b/source/DasBlog.Services/ConfigFile/SiteConfig.cs @@ -177,7 +177,19 @@ public SiteConfig() { } public string SpamBlockingServiceApiKey { get; set; } [XmlIgnore] - public ISpamBlockingService SpamBlockingService { get; set; } + public ISpamBlockingService SpamBlockingService + { + get + { + //TODO: this may eventually be configurable, if Akismet alternatives show up + if (! EnableSpamBlockingService|| SpamBlockingServiceApiKey.Length == 0) + { + return null; + } + return new AkismetSpamBlockingService(this.SpamBlockingServiceApiKey, this.Root); + } + set {} + } public bool EnableSpamModeration { get; set; } public int EntriesPerPage { get; set; } public bool EnableDailyReportEmail { get; set; } diff --git a/source/DasBlog.Services/DasBlog.Services.csproj b/source/DasBlog.Services/DasBlog.Services.csproj index 2e0e5d02..3b75cd0c 100644 --- a/source/DasBlog.Services/DasBlog.Services.csproj +++ b/source/DasBlog.Services/DasBlog.Services.csproj @@ -6,6 +6,7 @@ + diff --git a/source/DasBlog.Services/SpamBlocking/AkismetSpamBlockingService.cs b/source/DasBlog.Services/SpamBlocking/AkismetSpamBlockingService.cs new file mode 100644 index 00000000..d0a7f965 --- /dev/null +++ b/source/DasBlog.Services/SpamBlocking/AkismetSpamBlockingService.cs @@ -0,0 +1,77 @@ +using System; +using newtelligence.DasBlog.Runtime; +using Subtext.Akismet; +using AkismetComment = Subtext.Akismet.Comment; + +namespace DasBlog.Services +{ + /// + /// Uses the Akismet service to determine if a comment is SPAM + /// + /// http://akismet.com/ + public class AkismetSpamBlockingService : ISpamBlockingService + { + AkismetClient akismetClient; + + public AkismetSpamBlockingService(string apiKey, string blogUrl) + { + akismetClient = new AkismetClient(apiKey, new Uri(blogUrl)); + } + + public bool IsSpam(IFeedback feedback) + { + IComment akismetFormattedComment = ConvertToAkismetComment(feedback); + return akismetClient.CheckCommentForSpam(akismetFormattedComment); + } + + public void ReportSpam(IFeedback feedback) + { + IComment akismetFormattedComment = ConvertToAkismetComment(feedback); + akismetClient.SubmitSpam(akismetFormattedComment); + } + + public void ReportNotSpam(IFeedback feedback) + { + IComment akismetFormattedComment = ConvertToAkismetComment(feedback); + akismetClient.SubmitHam(akismetFormattedComment); + } + + private AkismetComment ConvertToAkismetComment(IFeedback feedback) + { + System.Net.IPAddress ipAddress = System.Net.IPAddress.None; + if (feedback.AuthorIPAddress != null) + { + try + { + ipAddress = System.Net.IPAddress.Parse(feedback.AuthorIPAddress); + } + catch(FormatException){} + } + AkismetComment comment = new AkismetComment(ipAddress, feedback.AuthorUserAgent); + comment.Author = feedback.Author; + comment.AuthorEmail = feedback.AuthorEmail; + if (feedback.AuthorHomepage != null && feedback.AuthorHomepage.Length > 0) + { + try + { + comment.AuthorUrl = new Uri(feedback.AuthorHomepage); + } + catch(UriFormatException){} + } + comment.Content = feedback.Content; + comment.Referer = feedback.Referer; + if (feedback.TargetEntryId != null & feedback.TargetEntryId.Trim().Length > 0) + { + try + { + //TODO: comeback + //comment.Permalink = new Uri(SiteUtilities.GetPermaLinkUrl(feedback.TargetEntryId)); + } + catch(UriFormatException){} + } + comment.CommentType = feedback.FeedbackType; + return comment; + } + + } +} diff --git a/source/DasBlog.Web.UI/Controllers/BlogPostController.cs b/source/DasBlog.Web.UI/Controllers/BlogPostController.cs index baa10932..013aa736 100644 --- a/source/DasBlog.Web.UI/Controllers/BlogPostController.cs +++ b/source/DasBlog.Web.UI/Controllers/BlogPostController.cs @@ -19,6 +19,7 @@ using System.Linq; using System.Net; using reCAPTCHA.AspNetCore; +using DasBlog.Services.ConfigFile.Interfaces; namespace DasBlog.Web.Controllers { @@ -434,18 +435,29 @@ public IActionResult AddComment(AddCommentViewModel addcomment) } } - if (errors.Count > 0) - { - return CommentError(addcomment, errors); - } - var commt = mapper.Map(addcomment); commt.AuthorIPAddress = HttpContext.Connection.RemoteIpAddress.ToString(); commt.AuthorUserAgent = HttpContext.Request.Headers["User-Agent"].ToString(); commt.EntryId = Guid.NewGuid().ToString(); commt.IsPublic = !dasBlogSettings.SiteConfiguration.CommentsRequireApproval; commt.CreatedUtc = commt.ModifiedUtc = DateTime.Now.ToUniversalTime(); - + if (dasBlogSettings.SiteConfiguration.EnableSpamBlockingService) + { + commt = CheckForSpam(commt, dasBlogSettings.SiteConfiguration); + // Spam Moderation is Disabled and the comment is spam. Let's show an error! + // TODO: Discuss what are the pros and cons of showing error vs just silently deleting the + // comment. + if (!dasBlogSettings.SiteConfiguration.EnableSpamModeration && commt.SpamState == NBR.SpamState.Spam) + { + errors.Add("Spam Comment Detected. Please enter a legitimate comment that is not spam to post it."); + } + } + + if (errors.Count > 0) + { + return CommentError(addcomment, errors); + } + logger.LogInformation(new EventDataItem(EventCodes.CommentAdded, null, "Comment CONTENT DUMP", commt.Content)); var state = blogManager.AddComment(addcomment.TargetEntryId, commt); @@ -484,6 +496,28 @@ public IActionResult AddComment(AddCommentViewModel addcomment) return Comment(addcomment.TargetEntryId); } + private NBR.Comment CheckForSpam(NBR.Comment commt, ISiteConfig siteConfiguration) + { + try + { + if (siteConfiguration.SpamBlockingService.IsSpam(commt)) + { + commt.SpamState = NBR.SpamState.Spam; + commt.IsPublic = false; + } + else + { + commt.SpamState = NBR.SpamState.NotSpam; + commt.IsPublic = true; + } + } + catch (Exception ex) + { + logger.LogError(new EventDataItem(EventCodes.Error, null, String.Format("The external spam blocking service failed for comment {0}. Original exception: {1}", commt.EntryId, ex))); + } + return commt; + } + [HttpDelete("post/{postid:guid}/comments/{commentid:guid}")] public IActionResult DeleteComment(Guid postid, Guid commentid) { @@ -586,7 +620,7 @@ private IActionResult HandleNewCategory(PostViewModel post) var newCategory = post.NewCategory?.Trim(); var newCategoryDisplayName = newCategory; var newCategoryUrl = NBR.Entry.InternalCompressTitle(newCategory); - // Category names should not include special characters #200 + // Category names should not include special characters #200 if (post.AllCategories.Any(c => c.CategoryUrl == newCategoryUrl)) { ModelState.AddModelError(nameof(post.NewCategory), $"The category, {post.NewCategory}, already exists"); diff --git a/source/DasBlog.Web.UI/Models/AdminViewModels/SiteViewModel.cs b/source/DasBlog.Web.UI/Models/AdminViewModels/SiteViewModel.cs index a5841984..b1a3f6ad 100644 --- a/source/DasBlog.Web.UI/Models/AdminViewModels/SiteViewModel.cs +++ b/source/DasBlog.Web.UI/Models/AdminViewModels/SiteViewModel.cs @@ -355,9 +355,19 @@ public class SiteViewModel public string CommentsGravatarRating { get; set; } public bool CommentsAllowHtml { get; set; } public bool EnableCoComment { get; set; } + + [DisplayName("Enable Spam Blocking Service")] + [Description("Enable Akismet Spam Blocking Service.")] public bool EnableSpamBlockingService { get; set; } + + [DisplayName("Akismet API Key")] + [Description("API Key for Spam Blocking Service")] public string SpamBlockingServiceApiKey { get; set; } //public ISpamBlockingService SpamBlockingService { get; set; } + + + [DisplayName("Enable Spam Moderation")] + [Description("Allow Manual Moderation of Spam")] public bool EnableSpamModeration { get; set; } public bool EnableDailyReportEmail { get; set; } public bool EnableGoogleMaps { get; set; } diff --git a/source/DasBlog.Web.UI/Views/Admin/Settings.cshtml b/source/DasBlog.Web.UI/Views/Admin/Settings.cshtml index 79ceb3c9..077df450 100644 --- a/source/DasBlog.Web.UI/Views/Admin/Settings.cshtml +++ b/source/DasBlog.Web.UI/Views/Admin/Settings.cshtml @@ -304,6 +304,36 @@ + + + + + @Html.CheckBoxFor(m => @Model.SiteConfig.EnableSpamBlockingService, new { @class = "form-check-input form-check-input" }) + + @Html.LabelFor(m => @Model.SiteConfig.EnableSpamBlockingService, null, new { @class = "col-check-label col-sm-10" }) + + + + + + + @Html.CheckBoxFor(m => @Model.SiteConfig.EnableSpamModeration, new { @class = "form-check-input form-check-input" }) + + @Html.LabelFor(m => @Model.SiteConfig.EnableSpamModeration, null, new { @class = "col-check-label col-sm-10" }) + + + + + + @Html.LabelFor(m => @Model.SiteConfig.SpamBlockingServiceApiKey, null, new { @class = "col-form-label col-sm-2" }) + + @Html.TextBoxFor(m => @Model.SiteConfig.SpamBlockingServiceApiKey, null, new { @class = "form-control sm-10" }) + + @Html.ValidationMessageFor(m => m.SiteConfig.SpamBlockingServiceApiKey, null, new { @class = "text-danger" }) + + + + @@ -779,10 +809,6 @@ @Html.HiddenFor(@m => m.SiteConfig.CommentsGravatarRating) @Html.HiddenFor(@m => m.SiteConfig.EnableCoComment) - @Html.HiddenFor(@m => m.SiteConfig.EnableSpamBlockingService) - @Html.HiddenFor(@m => m.SiteConfig.SpamBlockingServiceApiKey) - @Html.HiddenFor(@m => m.SiteConfig.EnableSpamModeration) - @Html.HiddenFor(@m => m.SiteConfig.EnableDailyReportEmail) @Html.HiddenFor(@m => m.SiteConfig.EnableGoogleMaps) diff --git a/source/Subtext.Akismet/Subtext.Akismet.csproj b/source/Subtext.Akismet/Subtext.Akismet.csproj index 663ee715..914b0937 100644 --- a/source/Subtext.Akismet/Subtext.Akismet.csproj +++ b/source/Subtext.Akismet/Subtext.Akismet.csproj @@ -1,17 +1,8 @@ - - + Local - 8.0.50727 - 2.0 - {4DC29536-2515-4B8D-AEB6-EC397B950881} - Debug - AnyCPU - - - Subtext.Akismet JScript @@ -19,16 +10,9 @@ IE50 false Library - Subtext.Akismet OnBuildSuccess - - - - - 4.0 - v4.7.2 publish\ true Disk @@ -44,86 +28,41 @@ false false true - - bin\Debug\ - false 285212672 - false - DEBUG;TRACE - true 4096 false - false false false - false - 4 - full - prompt - - false + + - bin\Release\ - false 285212672 - false - TRACE - false 4096 false - true false false - false - 4 none - prompt AllRules.ruleset - false - + System - - System.Web - - - - - Properties\AssemblyInfo.cs - Code - - - Code - - - Code - - - Code - - - Code - - - Code - @@ -142,11 +81,12 @@ true - + - - - - + net5.0 + false + + + \ No newline at end of file