From 0d2f7b7ff9bbb3da857aca68591ad775507a835e Mon Sep 17 00:00:00 2001 From: Hamid Reza Mohammadi Date: Mon, 20 Jul 2026 18:35:59 +0330 Subject: [PATCH] adding html sanitizer to comments --- RMuseum/RMuseum.csproj | 1 + .../GanjoorService-Maintenance.cs | 426 +++++++++--------- 2 files changed, 202 insertions(+), 225 deletions(-) diff --git a/RMuseum/RMuseum.csproj b/RMuseum/RMuseum.csproj index c4837c16..0fe20d76 100644 --- a/RMuseum/RMuseum.csproj +++ b/RMuseum/RMuseum.csproj @@ -21,6 +21,7 @@ + all diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Maintenance.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Maintenance.cs index 1d5d64e0..7ff7b6d0 100644 --- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Maintenance.cs +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Maintenance.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using Ganss.Xss; +using Microsoft.EntityFrameworkCore; using RMuseum.DbContext; using RMuseum.Models.Ganjoor; using RSecurityBackend.Models.Generic; @@ -9,6 +10,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; using System.Net.Http; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace RMuseum.Services.Implementation @@ -132,7 +134,7 @@ namespace RMuseum.Services.Implementation await _FindCategoryPoemsRhythmsMoreInternal(context, jobProgressServiceEF, job, subCat.Id, retag, rhythm); } - + } catch (Exception exp) { @@ -329,28 +331,28 @@ namespace RMuseum.Services.Implementation await context.SaveChangesAsync(); } else - if (url.IndexOf("https://ganjoor.net") == 0) - { - var testUrl = url.Substring("https://ganjoor.net".Length); - if (testUrl[testUrl.Length - 1] == '/') - testUrl = testUrl.Substring(0, testUrl.Length - 1); - var pageCount = await context.GanjoorPages.Where(p => p.FullUrl == testUrl).CountAsync(); - if (pageCount != 1) + if (url.IndexOf("https://ganjoor.net") == 0) { - context.GanjoorHealthCheckErrors.Add - ( - new GanjoorHealthCheckError() - { - ReferrerPageUrl = pages[i].FullUrl, - TargetUrl = url, - BrokenLink = pageCount == 0, - MulipleTargets = pageCount != 0 - } - ); + var testUrl = url.Substring("https://ganjoor.net".Length); + if (testUrl[testUrl.Length - 1] == '/') + testUrl = testUrl.Substring(0, testUrl.Length - 1); + var pageCount = await context.GanjoorPages.Where(p => p.FullUrl == testUrl).CountAsync(); + if (pageCount != 1) + { + context.GanjoorHealthCheckErrors.Add + ( + new GanjoorHealthCheckError() + { + ReferrerPageUrl = pages[i].FullUrl, + TargetUrl = url, + BrokenLink = pageCount == 0, + MulipleTargets = pageCount != 0 + } + ); - await context.SaveChangesAsync(); + await context.SaveChangesAsync(); + } } - } } } @@ -370,223 +372,197 @@ namespace RMuseum.Services.Implementation private async Task _ProcessCommentHtml(string commentText, RMuseumDbContext context) { - string[] allowedTags = new string[] - { - "p", - "a", - "br", - "b", - "i", - "strong", - "img" - }; - if (commentText.IndexOf("<") != -1) - { - int openTagIndex = commentText.IndexOf('<'); - while (openTagIndex != -1) - { - int closeOpenningTagIndex = commentText.IndexOf('>', openTagIndex + 1); - if (closeOpenningTagIndex == -1) //an unclosed tag - { - if (commentText.IndexOf(' ', openTagIndex + 1) != -1) - { - commentText = commentText.Substring(0, openTagIndex) + commentText.Substring(commentText.IndexOf(' ', openTagIndex + 1)); - } - else - { - commentText = commentText.Substring(0, openTagIndex); - } - } - else - { - int anotherOpenTagInBetweenIndex = commentText.IndexOf('<', openTagIndex + 1); - if (anotherOpenTagInBetweenIndex != -1 && anotherOpenTagInBetweenIndex < closeOpenningTagIndex) - { - commentText = commentText.Substring(0, openTagIndex) + commentText.Substring(anotherOpenTagInBetweenIndex); - } - else - { - int tagTypeCloseIndex = closeOpenningTagIndex; - int spaceAfterOpenningTagIndex = commentText.IndexOf(' ', openTagIndex + 1); - if (spaceAfterOpenningTagIndex != -1 && spaceAfterOpenningTagIndex < tagTypeCloseIndex) - tagTypeCloseIndex = spaceAfterOpenningTagIndex; + // Use a proper HTML sanitizer + var sanitizer = new HtmlSanitizer(); + // Configure allowed tags + sanitizer.AllowedTags.Clear(); + sanitizer.AllowedTags.Add("p"); + sanitizer.AllowedTags.Add("a"); + sanitizer.AllowedTags.Add("br"); + sanitizer.AllowedTags.Add("b"); + sanitizer.AllowedTags.Add("i"); + sanitizer.AllowedTags.Add("strong"); + sanitizer.AllowedTags.Add("img"); + sanitizer.AllowedTags.Add("span"); - string tagType = commentText.Substring(openTagIndex + 1, tagTypeCloseIndex - openTagIndex - 1).ToLower(); - tagType = tagType.Replace("/", "");//include close tags - if (tagType.Length == 0) - { - if (closeOpenningTagIndex == commentText.Length - 1) - commentText = commentText.Substring(0, openTagIndex); - else - commentText = commentText.Substring(0, openTagIndex) + commentText.Substring(closeOpenningTagIndex + 1); - } - else - { - if (!allowedTags.Contains(tagType)) - { - commentText = commentText.Substring(0, openTagIndex) + commentText.Substring(closeOpenningTagIndex + 1); - commentText = commentText.Replace($"", ""); - } - } - } - } + // Configure allowed attributes + sanitizer.AllowedAttributes.Clear(); + sanitizer.AllowedAttributes.Add("href"); + sanitizer.AllowedAttributes.Add("src"); + sanitizer.AllowedAttributes.Add("alt"); + sanitizer.AllowedAttributes.Add("title"); + sanitizer.AllowedAttributes.Add("rel"); + // IMPORTANT: Remove all style attributes to prevent colored text and font changes + sanitizer.AllowedAttributes.Remove("style"); - openTagIndex = commentText.IndexOf("<", openTagIndex + 1); - } - } + // Disallow any CSS or style-related attributes + sanitizer.AllowedCssProperties.Clear(); - if (commentText.IndexOf("href=") == -1 && commentText.IndexOf("http") != -1) - { - commentText = _Linkify(commentText); - } - int index = commentText.IndexOf("href="); - while (index != -1) - { - index += "href=\"".Length; - commentText = commentText.Replace("'", "\""); - if (commentText.IndexOf("\"", index) != -1) - { - int closeIndex = commentText.IndexOf("\"", index); - if (closeIndex == -1) - { - continue; - } - string url = commentText.Substring(index, closeIndex - index); - closeIndex = commentText.IndexOf(">", index); - if (closeIndex != -1 && commentText.IndexOf("", closeIndex) != -1) - { - closeIndex += ">".Length; - string urlText = commentText.Substring(closeIndex, commentText.IndexOf("", closeIndex) - closeIndex); - if (urlText == url) - { - bool textFixed = false; - if (urlText.IndexOf("http://ganjoor.net") == 0 || urlText.IndexOf("https://ganjoor.net") == 0) - { - urlText = urlText.Replace("http://ganjoor.net", "").Replace("https://ganjoor.net", ""); - int coupletNumber = -1; - if (urlText.IndexOf("#bn") != -1) - { - int coupletStartIndex = urlText.IndexOf("#bn") + "#bn".Length; - if (int.TryParse(urlText.Substring(coupletStartIndex), out coupletNumber)) - { - urlText = urlText.Substring(0, urlText.IndexOf("#bn")); - } - } - if (urlText.Length > 0 && urlText[urlText.Length - 1] == '/') - urlText = urlText.Substring(0, urlText.Length - 1); - var page = await context.GanjoorPages.AsNoTracking().Where(p => p.FullUrl == urlText).FirstOrDefaultAsync(); - if (page != null) - { - if (coupletNumber != -1) - { - string coupletSummary = ""; - int coupletIndex = coupletNumber - 1; - var verses = await _context.GanjoorVerses.Where(v => v.PoemId == page.Id).OrderBy(v => v.VOrder).ToListAsync(); - int cIndex = -1; - for (int i = 0; i < verses.Count; i++) - { - if (verses[i].VersePosition != VersePosition.Left && verses[i].VersePosition != VersePosition.CenteredVerse2) - cIndex++; - if (cIndex == coupletIndex) - { - coupletSummary = verses[i].Text; - if (verses[i].VersePosition == VersePosition.Right) - { - if (i < verses.Count - 1) - { - coupletSummary += $" {verses[i + 1].Text}"; - } - } - if (verses[i].VersePosition == VersePosition.CenteredVerse1) - { - if (i < verses.Count - 1) - { - if (verses[i + 1].VersePosition == VersePosition.CenteredVerse2) - { - coupletSummary += $" {verses[i + 1].Text}"; - } - } - } - break; - } - } - if (!string.IsNullOrEmpty(coupletSummary)) - { - coupletSummary = _CutSummary(coupletSummary); - commentText = commentText.Substring(0, closeIndex) + page.FullTitle + " » " + coupletSummary + commentText.Substring(commentText.IndexOf("", closeIndex)); - textFixed = true; - } - else - coupletNumber = -1; - } - if (coupletNumber == -1) - { - commentText = commentText.Substring(0, closeIndex) + page.FullTitle + commentText.Substring(commentText.IndexOf("", closeIndex)); - textFixed = true; - } - } + // Additional security: disallow data URIs and javascript: protocols + sanitizer.AllowedSchemes.Clear(); + sanitizer.AllowedSchemes.Add("http"); + sanitizer.AllowedSchemes.Add("https"); + sanitizer.AllowedSchemes.Add("mailto"); + sanitizer.AllowedSchemes.Add("ftp"); - } - if (!textFixed) - commentText = commentText.Substring(0, closeIndex) + "پیوند به وبگاه بیرونی" + commentText.Substring(commentText.IndexOf("", closeIndex)); - } - } - } - index = commentText.IndexOf("href=\"", index); - } - return commentText; + // Sanitize the HTML + string sanitizedHtml = sanitizer.Sanitize(commentText); + + // Process URLs (Linkify) and internal Ganjoor links + sanitizedHtml = await _ProcessUrls(sanitizedHtml, context); + + return sanitizedHtml; } - private string _Linkify(string SearchText) + private async Task _ProcessUrls(string html, RMuseumDbContext context) { - if (SearchText.IndexOf("href") != -1) - return SearchText; - int linkIndex = SearchText.IndexOf("http"); - while (linkIndex != -1) + // First, process any existing href links + html = await _ProcessExistingLinks(html, context); + + // Then linkify any bare URLs that aren't already in links + if (!html.Contains("href=\"") && html.Contains("http")) { - int linkEndIndex = SearchText.IndexOfAny(new char[] { '\r', '\n', '<', ' ' }, linkIndex); - if (linkEndIndex == -1) - linkEndIndex = SearchText.Length - 1; - if (linkEndIndex != -1) + html = _Linkify(html); + } + + return html; + } + + private async Task _ProcessExistingLinks(string html, RMuseumDbContext context) + { + // Use regex to find and process all href attributes + var hrefRegex = new Regex(@"]*?\s+)?href=""([^""]*)""[^>]*>(.*?)", RegexOptions.IgnoreCase | RegexOptions.Singleline); + + var matches = hrefRegex.Matches(html); + var result = html; + var replacements = new List<(string old, string @new)>(); + + foreach (Match match in matches) + { + string url = match.Groups[1].Value; + string linkText = match.Groups[2].Value; + string newLink = await _ProcessSingleLink(url, linkText, context); + + // Store the replacement + replacements.Add((match.Value, newLink)); + } + + // Apply replacements from end to start to maintain indices + foreach (var replacement in replacements.OrderByDescending(r => result.IndexOf(r.old))) + { + result = result.Replace(replacement.old, replacement.@new); + } + + return result; + } + + private async Task _ProcessSingleLink(string url, string linkText, RMuseumDbContext context) + { + // If link text is the same as URL (auto-generated link), try to improve it + if (url == linkText || linkText.Trim() == url.Trim()) + { + // Process Ganjoor internal links + if (url.StartsWith("http://ganjoor.net") || url.StartsWith("https://ganjoor.net")) { - string link = SearchText.Substring(linkIndex, linkEndIndex - linkIndex); - SearchText - = - SearchText.Substring(0, linkIndex) - + - "" - + - link - + - "" - + - SearchText.Substring(linkEndIndex); - linkIndex = - ( - SearchText.Substring(0, linkIndex) - + - "" - + - link - + - "" - ).Length; - linkIndex = SearchText.IndexOf("http", linkIndex); + string path = url.Replace("http://ganjoor.net", "").Replace("https://ganjoor.net", ""); + int coupletNumber = -1; + string cleanPath = path; + + // Extract couplet number if present + if (path.Contains("#bn")) + { + int coupletStartIndex = path.IndexOf("#bn") + "#bn".Length; + if (int.TryParse(path.Substring(coupletStartIndex), out coupletNumber)) + { + cleanPath = path.Substring(0, path.IndexOf("#bn")); + } + } + + // Remove trailing slash + if (cleanPath.Length > 0 && cleanPath[cleanPath.Length - 1] == '/') + cleanPath = cleanPath.Substring(0, cleanPath.Length - 1); + + var page = await context.GanjoorPages + .AsNoTracking() + .Where(p => p.FullUrl == cleanPath) + .FirstOrDefaultAsync(); + + if (page != null) + { + string displayText = page.FullTitle; + + // Add couplet summary if applicable + if (coupletNumber != -1) + { + string coupletSummary = await _GetCoupletSummary(page.Id, coupletNumber); + if (!string.IsNullOrEmpty(coupletSummary)) + { + displayText = $"{page.FullTitle} » {coupletSummary}"; + } + } + + return $@"{displayText}"; + } } else - linkIndex = SearchText.IndexOf("http", linkIndex + "http".Length); + { + // External link - use generic text + return $@"پیوند به وبگاه بیرونی"; + } } - return SearchText; + + // If link text was manually provided, keep it but still validate the link + return $@"{linkText}"; + } + + private async Task _GetCoupletSummary(int poemId, int coupletNumber) + { + int coupletIndex = coupletNumber - 1; + var verses = await _context.GanjoorVerses + .Where(v => v.PoemId == poemId) + .OrderBy(v => v.VOrder) + .ToListAsync(); + + int cIndex = -1; + for (int i = 0; i < verses.Count; i++) + { + if (verses[i].VersePosition != VersePosition.Left && + verses[i].VersePosition != VersePosition.CenteredVerse2) + cIndex++; + + if (cIndex == coupletIndex) + { + string coupletSummary = verses[i].Text; + + if (verses[i].VersePosition == VersePosition.Right && i < verses.Count - 1) + { + coupletSummary += $" {verses[i + 1].Text}"; + } + else if (verses[i].VersePosition == VersePosition.CenteredVerse1 && + i < verses.Count - 1 && + verses[i + 1].VersePosition == VersePosition.CenteredVerse2) + { + coupletSummary += $" {verses[i + 1].Text}"; + } + + return _CutSummary(coupletSummary); + } + } + + return null; + } + + private string _Linkify(string text) + { + // Improved Linkify with better URL detection + var urlRegex = new Regex(@"(https?://[^\s<>""']+)", RegexOptions.IgnoreCase); + return urlRegex.Replace(text, match => + { + string url = match.Value; + return $@"{url}"; + }); } /// @@ -706,7 +682,7 @@ namespace RMuseum.Services.Implementation } catch (Exception exp) { - return new RServiceResult(false, exp.ToString() ); + return new RServiceResult(false, exp.ToString()); } }