diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Renderers/IMarkdownToHtmlRenderer.cs b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Renderers/IMarkdownToHtmlRenderer.cs new file mode 100644 index 0000000000..98fcda1fac --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Renderers/IMarkdownToHtmlRenderer.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.CmsKit.Web.Renderers; + +public interface IMarkdownToHtmlRenderer +{ + Task RenderAsync(string rawMarkdown, bool preventXSS = true); +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Renderers/MarkdownToHtmlRenderer.cs b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Renderers/MarkdownToHtmlRenderer.cs new file mode 100644 index 0000000000..639948e801 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Renderers/MarkdownToHtmlRenderer.cs @@ -0,0 +1,129 @@ +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.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 async Task RenderAsync(string rawMarkdown, bool preventXSS = false) + { + 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.Common.Web/Volo.CmsKit.Common.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj index 1ca9a78b27..e717f49be5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj @@ -1,38 +1,40 @@ - - + + - - net6.0 - $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; - true - Library - Volo.CmsKit.Web - true - + + net6.0 + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + true + Library + Volo.CmsKit.Web + true + - - - - - + + + + + - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + +