commit 3c1858413fee3da978dda88c923bad31ed6d3bb4
Author: Vojtěch Bartička <aitakaitov@gmail.com>
Date:   Sun Apr 17 12:11:16 2022 +0200

    HTML preprocessing and sanitization

diff --git a/Backend/Core/Core.csproj b/Backend/Core/Core.csproj
index bf8842f..717cd8b 100644
--- a/Backend/Core/Core.csproj
+++ b/Backend/Core/Core.csproj
@@ -10,6 +10,8 @@
     <PackageReference Include="AutoMapper" Version="11.0.1" />
     <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
     <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
+    <PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
+    <PackageReference Include="HtmlSanitizer" Version="7.1.488" />
     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.3" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3">
diff --git a/Backend/Core/Services/AnnotationService/AnnotationServiceEF.cs b/Backend/Core/Services/AnnotationService/AnnotationServiceEF.cs
index 8ec1681..3023400 100644
--- a/Backend/Core/Services/AnnotationService/AnnotationServiceEF.cs
+++ b/Backend/Core/Services/AnnotationService/AnnotationServiceEF.cs
@@ -11,6 +11,9 @@ using System.Threading.Tasks;
 using Microsoft.EntityFrameworkCore;
 using AutoMapper;
 using Models.Tags;
+using Ganss.XSS;
+using HtmlAgilityPack;
+using System.Text.RegularExpressions;
 
 namespace Core.Services.AnnotationService
 {
@@ -98,7 +101,7 @@ namespace Core.Services.AnnotationService
                 .Include(a => a.User)
                 .Include(a => a.Document).ThenInclude(d => d.Content)
                 .First();
-            
+
             if (userRole < ERole.ADMINISTRATOR)
             {
                 if (annotation.User.Id != userId)
@@ -108,11 +111,15 @@ namespace Core.Services.AnnotationService
             }
 
             var documentContent = context.Documents.Where(d => d.Id == annotation.Document.Id).Select(d => d.Content).First();
+            var preprocessingResult = PreprocessHTML(documentContent.Content);
 
             // We probably cannot use AutoMapper since we are dealing with too many different entities
             AnnotationInfo annotationInfo = new()
             {
-                DocumentText = documentContent.Content,
+                SourceDocumentContent = documentContent.Content,
+                DocumentToRender = preprocessingResult.docToRender,
+                TagStartPositions = preprocessingResult.openingTagPositions,
+                TagLengths = preprocessingResult.openingTagLengths,
                 Note = annotation.Note,
                 State = annotation.State,
                 Type = IsHtml(documentContent.Content) ? EDocumentType.HTML : EDocumentType.TEXT
@@ -132,6 +139,57 @@ namespace Core.Services.AnnotationService
             return annotationInfo;
         }
 
+        private (string docToRender, int[] openingTagPositions, int[] openingTagLengths, int[] closingTagPositions, int[] closingTagLengths) PreprocessHTML(string htmlSource)
+        {
+            var docOriginal = new HtmlDocument();
+            docOriginal.LoadHtml(htmlSource);
+            var docToEdit = new HtmlDocument();
+            docToEdit.LoadHtml(htmlSource);
+
+            string idAttributeName = "aswi-tag-id";
+
+            var descendantsOriginal = docOriginal.DocumentNode.DescendantsAndSelf();
+            var descendantsToEdit = docToEdit.DocumentNode.DescendantsAndSelf();
+
+            List<int> tagStartPositions = new();
+            List<int> tagStartLengths = new();
+
+
+            List<int> tagClosingPositions = new();
+            List<int> tagClosingLengths = new();
+
+            int currentId = 0;
+
+            var zipped = descendantsOriginal.Zip(descendantsToEdit, (orig, toEdit) => new { Original = orig, ToEdit = toEdit });
+            foreach (var node in zipped)
+            {
+                var originalNode = node.Original;
+                var toEditNode = node.ToEdit;
+
+                if (originalNode.Name.Contains("#"))
+                {
+                    continue;
+                }
+
+                tagStartPositions.Add(originalNode.OuterStartIndex);
+                tagStartLengths.Add(originalNode.OuterStartIndex - originalNode.InnerStartIndex);
+                currentId = tagStartPositions.Count - 1;
+                toEditNode.Attributes.Add(idAttributeName, currentId.ToString());
+
+                tagClosingPositions.Add(originalNode.InnerStartIndex + originalNode.InnerLength);
+                tagClosingLengths.Add((originalNode.OuterStartIndex + originalNode.OuterLength) - (originalNode.InnerStartIndex + originalNode.InnerLength));
+            }
+
+            string docToRender = docToEdit.DocumentNode.OuterHtml;
+            HtmlSanitizer sanitizer = new HtmlSanitizer();
+            sanitizer.AllowedAttributes.Clear();
+            sanitizer.AllowedAttributes.Add(idAttributeName);
+
+            docToRender = sanitizer.Sanitize(docToRender);
+
+            return (docToRender, tagStartPositions.ToArray(), tagStartLengths.ToArray(), tagClosingPositions.ToArray(), tagClosingLengths.ToArray());
+        }
+
         // TODO temporary
         private bool IsHtml(string text)
         {
diff --git a/Backend/Core/Services/DocumentService/DocumentServiceEF.cs b/Backend/Core/Services/DocumentService/DocumentServiceEF.cs
index 7c38428..b148923 100644
--- a/Backend/Core/Services/DocumentService/DocumentServiceEF.cs
+++ b/Backend/Core/Services/DocumentService/DocumentServiceEF.cs
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
 using System.Web;
 using AutoMapper;
 using Models.Users;
+using Ganss.XSS;
 
 namespace Core.Services.DocumentService
 {
diff --git a/Backend/Models/Annotations/AnnotationInfo.cs b/Backend/Models/Annotations/AnnotationInfo.cs
index ee6a1af..68f232f 100644
--- a/Backend/Models/Annotations/AnnotationInfo.cs
+++ b/Backend/Models/Annotations/AnnotationInfo.cs
@@ -10,7 +10,10 @@ namespace Models.Annotations
 {
     public class AnnotationInfo
     {
-        public string DocumentText { get; set; }
+        public string SourceDocumentContent { get; set; }
+        public string DocumentToRender { get; set; }
+        public int[] TagStartPositions { get; set; }
+        public int[] TagLengths { get; set; }
         public EState State { get; set; }
         public EDocumentType Type { get; set; }
         public string Note { get; set; }
