diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.codegen.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.codegen.cs index b660af0498..5165db1451 100644 --- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.codegen.cs +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.codegen.cs @@ -16,9 +16,9 @@ namespace __GeneratedComponent { builder.OpenElement(0, "div"); builder.AddAttribute(1, "class", this.ToString()); - builder.AddContent(2, "\r\n Hello world\r\n "); + builder.AddMarkupContent(2, "\r\n Hello world\r\n "); builder.AddContent(3, string.Format("{0}", "Hello")); - builder.AddContent(4, "\r\n"); + builder.AddMarkupContent(4, "\r\n"); builder.CloseElement(); } #pragma warning restore 1998 diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMarkupEncodingPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMarkupEncodingPass.cs new file mode 100644 index 0000000000..152d0eef33 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMarkupEncodingPass.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ComponentMarkupEncodingPass : ComponentIntermediateNodePassBase, IRazorOptimizationPass + { + // Runs after ComponentMarkupBlockPass + public override int Order => 10010; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + if (!IsComponentDocument(documentNode)) + { + return; + } + + if (documentNode.Options.DesignTime) + { + // Nothing to do during design time. + return; + } + + var rewriter = new Rewriter(); + rewriter.Visit(documentNode); + } + + private class Rewriter : IntermediateNodeWalker + { + // Markup content in components are rendered in one of the following two ways, + // AddContent - we encode it when used with prerendering and inserted into the DOM in a safe way (low perf impact) + // AddMarkupContent - renders the content directly as markup (high perf impact) + // Because of this, we want to use AddContent as much as possible. + // + // We want to use AddMarkupContent to avoid aggresive encoding during prerendering. + // Specifically, when one of the following characters are in the content, + // 1. New lines (\r, \n), tabs(\t) - so they get rendered as actual new lines, tabs instead of + // 2. Ampersands (&) - so that HTML entities are rendered correctly without getting encoded + // 3. Any character outside the ASCII range + + private static readonly char[] EncodedCharacters = new[] { '\r', '\n', '\t', '&' }; + + public override void VisitHtml(HtmlContentIntermediateNode node) + { + for (var i = 0; i < node.Children.Count; i++) + { + var child = node.Children[i]; + if (!(child is IntermediateToken token) || !token.IsHtml) + { + // We only care about Html tokens. + continue; + } + + for (var j = 0; j < token.Content.Length; j++) + { + var ch = token.Content[j]; + // ASCII range is 0 - 127 + if (ch > 127 || EncodedCharacters.Contains(ch)) + { + node.SetEncoded(); + return; + } + } + } + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentRuntimeNodeWriter.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentRuntimeNodeWriter.cs index 33b29255f4..72e18f24cc 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentRuntimeNodeWriter.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentRuntimeNodeWriter.cs @@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components // Since we're not in the middle of writing an element, this must evaluate as some // text to display context.CodeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddContent)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{ComponentsApi.RenderTreeBuilder.AddContent}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator(); @@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } context.CodeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddMarkupContent)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{ComponentsApi.RenderTreeBuilder.AddMarkupContent}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator() .WriteStringLiteral(node.Content) @@ -178,7 +178,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } context.CodeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.OpenElement)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{ComponentsApi.RenderTreeBuilder.OpenElement}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator() .WriteStringLiteral(node.TagName) @@ -255,8 +255,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Components // Text node var content = GetHtmlContent(node); + var renderApi = ComponentsApi.RenderTreeBuilder.AddContent; + if (node.IsEncoded()) + { + // This content is already encoded. + renderApi = ComponentsApi.RenderTreeBuilder.AddMarkupContent; + } + context.CodeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddContent)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{renderApi}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator() .WriteStringLiteral(content) @@ -644,8 +651,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components var codeWriter = context.CodeWriter; var methodName = node.IsComponentCapture - ? nameof(ComponentsApi.RenderTreeBuilder.AddComponentReferenceCapture) - : nameof(ComponentsApi.RenderTreeBuilder.AddElementReferenceCapture); + ? ComponentsApi.RenderTreeBuilder.AddComponentReferenceCapture + : ComponentsApi.RenderTreeBuilder.AddElementReferenceCapture; codeWriter .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{methodName}") .Write((_sourceSequence++).ToString()) @@ -693,7 +700,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components protected override void BeginWriteAttribute(CodeWriter codeWriter, string key) { codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddAttribute)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{ComponentsApi.RenderTreeBuilder.AddAttribute}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator() .WriteStringLiteral(key) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Intermediate/HtmlContentIntermediateNodeExtensions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Intermediate/HtmlContentIntermediateNodeExtensions.cs new file mode 100644 index 0000000000..5ca25f851d --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Intermediate/HtmlContentIntermediateNodeExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Razor.Language.Intermediate +{ + internal static class HtmlContentIntermediateNodeExtensions + { + private static readonly string HasEncodedContent = "HasEncodedContent"; + + public static bool IsEncoded(this HtmlContentIntermediateNode node) + { + return ReferenceEquals(node.Annotations[HasEncodedContent], HasEncodedContent); + } + + public static void SetEncoded(this HtmlContentIntermediateNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + node.Annotations[HasEncodedContent] = HasEncodedContent; + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs index f2269e692e..82ca3509a8 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs @@ -232,6 +232,7 @@ namespace Microsoft.AspNetCore.Razor.Language builder.Features.Add(new ComponentGenericTypePass()); builder.Features.Add(new ComponentChildContentDiagnosticPass()); builder.Features.Add(new ComponentMarkupBlockPass()); + builder.Features.Add(new ComponentMarkupEncodingPass()); } private static void LoadExtensions(RazorProjectEngineBuilder builder, IReadOnlyList extensions)