diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json index fc7a1884ef..bb34fc3d29 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json @@ -158,6 +158,7 @@ "YourEmailAddress": "Your e-mail address", "YourFullName": "Your full name", "YourMessage": "Your Message", - "YourReply": "Your reply" + "YourReply": "Your reply", + "MarkdownSupported": "Markdown supported." } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json index 93a76580dc..24cf80fce8 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json @@ -158,6 +158,7 @@ "YourEmailAddress": "Email adresiniz", "YourFullName": "Tam adınız", "YourMessage": "Mesajınız", - "YourReply": "Cevabınız" + "YourReply": "Cevabınız", + "MarkdownSupported": "Markdown destekler." } -} \ No newline at end of file +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs index 411b088682..0a608740e9 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs @@ -8,6 +8,7 @@ using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; using Volo.CmsKit.Public.Comments; +using Volo.CmsKit.Public.Web.Renderers; namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting; @@ -21,13 +22,16 @@ namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting; public class CommentingViewComponent : AbpViewComponent { public ICommentPublicAppService CommentPublicAppService { get; } + public IMarkdownToHtmlRenderer MarkdownToHtmlRenderer { get; } public AbpMvcUiOptions AbpMvcUiOptions { get; } public CommentingViewComponent( ICommentPublicAppService commentPublicAppService, - IOptions options) + IOptions options, + IMarkdownToHtmlRenderer markdownToHtmlRenderer) { CommentPublicAppService = commentPublicAppService; + MarkdownToHtmlRenderer = markdownToHtmlRenderer; AbpMvcUiOptions = options.Value; } @@ -35,8 +39,9 @@ public class CommentingViewComponent : AbpViewComponent string entityType, string entityId) { - var result = await CommentPublicAppService - .GetListAsync(entityType, entityId); + var comments = (await CommentPublicAppService + .GetListAsync(entityType, entityId)).Items; + var loginUrl = $"{AbpMvcUiOptions.LoginUrl}?returnUrl={HttpContext.Request.Path.ToString()}&returnUrlHash=#cms-comment_{entityType}_{entityId}"; @@ -45,12 +50,31 @@ public class CommentingViewComponent : AbpViewComponent EntityId = entityId, EntityType = entityType, LoginUrl = loginUrl, - Comments = result.Items.OrderByDescending(i => i.CreationTime).ToList() + Comments = comments.OrderByDescending(i => i.CreationTime).ToList() }; + await ConvertMarkdownTextsToHtml(viewModel); + return View("~/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml", viewModel); } + private async Task ConvertMarkdownTextsToHtml(CommentingViewModel viewModel) + { + viewModel.RawCommentTexts = new Dictionary(); + + foreach (var comment in viewModel.Comments) + { + viewModel.RawCommentTexts.Add(comment.Id, comment.Text); + comment.Text = await MarkdownToHtmlRenderer.RenderAsync(comment.Text, true); + + foreach (var reply in comment.Replies) + { + viewModel.RawCommentTexts.Add(reply.Id, reply.Text); + reply.Text = await MarkdownToHtmlRenderer.RenderAsync(reply.Text, true); + } + } + } + public class CommentingViewModel { public string EntityType { get; set; } @@ -60,5 +84,8 @@ public class CommentingViewComponent : AbpViewComponent public string LoginUrl { get; set; } public IReadOnlyList Comments { get; set; } + + public Dictionary RawCommentTexts { get; set; } } } + diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml index ab4bd69b91..9d0438098d 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml @@ -49,6 +49,9 @@ } +
+ @L["MarkdownSupported"] +
; @@ -57,16 +60,7 @@ Func GetCommentContentArea(Guid id, string text) => @

- @{ - var lines = text.SplitToLines(); - if (lines.Any()) - { - foreach (var line in lines) - { - @line
- } - } - } + @Html.Raw(text)

; } @@ -114,9 +108,11 @@
@L["Update"] @L["Cancel"] -
+
+ @L["MarkdownSupported"] +
@@ -161,7 +157,7 @@ - @GetEditArea(comment.Id, comment.Text, comment.ConcurrencyStamp).Invoke(null) + @GetEditArea(comment.Id, Model.RawCommentTexts[comment.Id], comment.ConcurrencyStamp).Invoke(null) @if (comment.Replies.Any()) { @@ -191,7 +187,7 @@ - @GetEditArea(reply.Id, reply.Text, reply.ConcurrencyStamp).Invoke(null) + @GetEditArea(reply.Id, Model.RawCommentTexts[reply.Id], reply.ConcurrencyStamp).Invoke(null) } @@ -220,4 +216,3 @@ } - \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/IMarkdownToHtmlRenderer.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/IMarkdownToHtmlRenderer.cs index 9f19d9f3da..d11e76f6dd 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/IMarkdownToHtmlRenderer.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/IMarkdownToHtmlRenderer.cs @@ -4,5 +4,5 @@ namespace Volo.CmsKit.Public.Web.Renderers; public interface IMarkdownToHtmlRenderer { - Task RenderAsync(string rawMarkdown); + Task RenderAsync(string rawMarkdown, bool preventXSS = true); } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/MarkdownToHtmlRenderer.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/MarkdownToHtmlRenderer.cs index 6998a3d828..b6fab3c008 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/MarkdownToHtmlRenderer.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/MarkdownToHtmlRenderer.cs @@ -1,21 +1,129 @@ -using Markdig; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Markdig; using System.Threading.Tasks; +using System.Web; using Volo.Abp.DependencyInjection; +using Ganss.XSS; namespace Volo.CmsKit.Public.Web.Renderers; public class MarkdownToHtmlRenderer : IMarkdownToHtmlRenderer, ITransientDependency { + private readonly HtmlSanitizer _htmlSanitizer; protected MarkdownPipeline MarkdownPipeline { get; } public MarkdownToHtmlRenderer(MarkdownPipeline markdownPipeline) { MarkdownPipeline = markdownPipeline; + _htmlSanitizer = new HtmlSanitizer(); } - public Task RenderAsync(string rawMarkdown) + public async Task RenderAsync(string rawMarkdown, bool preventXSS = false) { - return Task.FromResult( - Markdown.ToHtml(rawMarkdown, MarkdownPipeline)); + if (preventXSS) + { + rawMarkdown = EncodeHtmlTags(rawMarkdown, true); + } + + var html = Markdown.ToHtml(rawMarkdown, MarkdownPipeline); + + if (preventXSS) + { + html = _htmlSanitizer.Sanitize(html); + } + + return html; + } + + + private static List GetCodeBlockIndices(string markdownText) + { + var regexObj = new Regex(@"```(\w)*|`(\w)*", RegexOptions.IgnoreCase | + RegexOptions.IgnorePatternWhitespace | + RegexOptions.Singleline | + RegexOptions.Multiline | + RegexOptions.ExplicitCapture); + + var matches = regexObj.Matches(markdownText); + var indices = new List(); + + for (var i = 0; i < matches.Count; i++) + { + if (!indices.Any() || indices.Last().EndIndex.HasValue) + { + indices.Add(new CodeBlockIndexPair(matches[i].Index)); + } + else + { + indices.Last().EndIndex = matches[i].Index; + } + } + + return indices; + } + + /// + /// Encodes html tags. + /// + private static string EncodeHtmlTags(string text, bool dontEncodeCodeBlocks = true) + { + List codeBlockIndices = null; + if (dontEncodeCodeBlocks) + { + codeBlockIndices = GetCodeBlockIndices(text); + } + + return Regex.Replace(text, @"<[^>]*>", match => + { + if (dontEncodeCodeBlocks && codeBlockIndices != null) + { + var isInCodeBlock = false; + foreach (var codeBlock in codeBlockIndices) + { + if (IsInCodeBlock(match.Index, codeBlock.StartIndex, codeBlock.EndIndex)) + { + isInCodeBlock = true; + break; + } + } + + if (isInCodeBlock) + { + return match.ToString(); + } + else + { + return HttpUtility.HtmlEncode(match.ToString()); + } + } + else + { + return HttpUtility.HtmlEncode(match.ToString()); + } + }); + } + + private static bool IsInCodeBlock(int currentIndex, int codeBlockStartIndex, int? codeBlockEndIndex) + { + if (codeBlockEndIndex.HasValue) + { + return (currentIndex >= codeBlockStartIndex && currentIndex <= codeBlockEndIndex); + } + + return currentIndex >= codeBlockStartIndex; + } + + private class CodeBlockIndexPair + { + public int StartIndex { get; private set; } + public int? EndIndex { get; set; } + + public CodeBlockIndexPair(int startIndex, int? endIndex = null) + { + StartIndex = startIndex; + EndIndex = endIndex; + } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj index 5ab0e7bf44..2dc1fd425c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj @@ -17,6 +17,7 @@ +