From 397f924e8da45975d38f5d2c2a1e097399365eba Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 5 Aug 2019 13:34:32 -0700 Subject: [PATCH] Ensure SystemTextJsonHelper always HTML encodes output. (#12808) * Ensure JsonSerializer always HTML encodes output. * Update JsonOptions.JsonSerializerOptions to use encoder scheme that does not encode non-ASCII characters by default. This makes the encoding comparable to Json.NET's defaults * In SystemTextJsonHelper, ensure that the content is always HTML-encoded * Unskip skipped test Fixes https://github.com/aspnet/AspNetCore/issues/9946 Fixes https://github.com/aspnet/AspNetCore/issues/11459 --- ...osoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs | 2 +- .../SystemTextJsonOutputFormatter.cs | 21 ++++- .../JsonSerializerOptionsCopyConstructor.cs | 36 +++++++++ .../Infrastructure/MvcCoreMvcOptionsSetup.cs | 4 +- .../test/CreatedAtActionResultTests.cs | 2 +- .../test/CreatedAtRouteResultTests.cs | 2 +- src/Mvc/Mvc.Core/test/CreatedResultTests.cs | 2 +- .../test/Formatters/FormatFilterTest.cs | 2 +- .../Formatters/JsonOutputFormatterTestBase.cs | 65 ++++++++++++++++ .../SystemTextJsonOutputFormatterTest.cs | 45 +---------- .../test/HttpNotFoundObjectResultTest.cs | 2 +- .../Mvc.Core/test/HttpOkObjectResultTest.cs | 2 +- .../src/Rendering/SystemTextJsonHelper.cs | 17 +++- .../test/Rendering/JsonHelperTestBase.cs | 26 ++++++- .../Rendering/SystemTextJsonHelperTest.cs | 77 +++++++++++++------ .../JsonOutputFormatterTestBase.cs | 18 ++++- .../SystemTextJsonOutputFormatterTest.cs | 23 +++++- .../JsonOutputFormatterController.cs | 3 + 18 files changed, 261 insertions(+), 88 deletions(-) create mode 100644 src/Mvc/Mvc.Core/src/Infrastructure/JsonSerializerOptionsCopyConstructor.cs diff --git a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs index 07646edce5..9f3472518d 100644 --- a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs @@ -2210,7 +2210,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } public partial class SystemTextJsonOutputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter { - public SystemTextJsonOutputFormatter(Microsoft.AspNetCore.Mvc.JsonOptions options) { } + public SystemTextJsonOutputFormatter(System.Text.Json.JsonSerializerOptions jsonSerializerOptions) { } public System.Text.Json.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] public sealed override System.Threading.Tasks.Task WriteResponseBodyAsync(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterWriteContext context, System.Text.Encoding selectedEncoding) { throw null; } diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs index 861f277e48..664615fa8d 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -17,14 +18,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// public class SystemTextJsonOutputFormatter : TextOutputFormatter { - /// /// Initializes a new instance. /// - /// The . - public SystemTextJsonOutputFormatter(JsonOptions options) + /// The . + public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) { - SerializerOptions = options.JsonSerializerOptions; + SerializerOptions = jsonSerializerOptions; SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); @@ -33,6 +33,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); } + internal static SystemTextJsonOutputFormatter CreateFormatter(JsonOptions jsonOptions) + { + var jsonSerializerOptions = jsonOptions.JsonSerializerOptions; + + if (jsonSerializerOptions.Encoder is null) + { + // If the user hasn't explicitly configured the encoder, use the less strict encoder that does not encode all non-ASCII characters. + jsonSerializerOptions = jsonSerializerOptions.Copy(JavaScriptEncoder.UnsafeRelaxedJsonEscaping); + } + + return new SystemTextJsonOutputFormatter(jsonSerializerOptions); + } + /// /// Gets the used to configure the . /// diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/JsonSerializerOptionsCopyConstructor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/JsonSerializerOptionsCopyConstructor.cs new file mode 100644 index 0000000000..f378aec55e --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Infrastructure/JsonSerializerOptionsCopyConstructor.cs @@ -0,0 +1,36 @@ +// 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.Text.Encodings.Web; + +namespace System.Text.Json +{ + internal static class JsonSerializerOptionsCopyConstructor + { + public static JsonSerializerOptions Copy(this JsonSerializerOptions serializerOptions, JavaScriptEncoder encoder) + { + var copiedOptions = new JsonSerializerOptions + { + AllowTrailingCommas = serializerOptions.AllowTrailingCommas, + DefaultBufferSize = serializerOptions.DefaultBufferSize, + DictionaryKeyPolicy = serializerOptions.DictionaryKeyPolicy, + IgnoreNullValues = serializerOptions.IgnoreNullValues, + IgnoreReadOnlyProperties = serializerOptions.IgnoreReadOnlyProperties, + MaxDepth = serializerOptions.MaxDepth, + PropertyNameCaseInsensitive = serializerOptions.PropertyNameCaseInsensitive, + PropertyNamingPolicy = serializerOptions.PropertyNamingPolicy, + ReadCommentHandling = serializerOptions.ReadCommentHandling, + WriteIndented = serializerOptions.WriteIndented + }; + + for (var i = 0; i < serializerOptions.Converters.Count; i++) + { + copiedOptions.Converters.Add(serializerOptions.Converters[i]); + } + + copiedOptions.Encoder = encoder; + + return copiedOptions; + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index cbc141fe76..1f246bdbd5 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -87,7 +87,9 @@ namespace Microsoft.AspNetCore.Mvc options.OutputFormatters.Add(new HttpNoContentOutputFormatter()); options.OutputFormatters.Add(new StringOutputFormatter()); options.OutputFormatters.Add(new StreamOutputFormatter()); - options.OutputFormatters.Add(new SystemTextJsonOutputFormatter(_jsonOptions.Value)); + + var jsonOutputFormatter = SystemTextJsonOutputFormatter.CreateFormatter(_jsonOptions.Value); + options.OutputFormatters.Add(jsonOutputFormatter); // Set up ValueProviders options.ValueProviderFactories.Add(new FormValueProviderFactory()); diff --git a/src/Mvc/Mvc.Core/test/CreatedAtActionResultTests.cs b/src/Mvc/Mvc.Core/test/CreatedAtActionResultTests.cs index be493c9eda..3b559992e5 100644 --- a/src/Mvc/Mvc.Core/test/CreatedAtActionResultTests.cs +++ b/src/Mvc/Mvc.Core/test/CreatedAtActionResultTests.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/CreatedAtRouteResultTests.cs b/src/Mvc/Mvc.Core/test/CreatedAtRouteResultTests.cs index 50eb7ba12b..f6cf7d583a 100644 --- a/src/Mvc/Mvc.Core/test/CreatedAtRouteResultTests.cs +++ b/src/Mvc/Mvc.Core/test/CreatedAtRouteResultTests.cs @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/CreatedResultTests.cs b/src/Mvc/Mvc.Core/test/CreatedResultTests.cs index 22e6b13637..56db1771c7 100644 --- a/src/Mvc/Mvc.Core/test/CreatedResultTests.cs +++ b/src/Mvc/Mvc.Core/test/CreatedResultTests.cs @@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/Formatters/FormatFilterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/FormatFilterTest.cs index fea2967990..3dd386d314 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/FormatFilterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/FormatFilterTest.cs @@ -465,7 +465,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Set up default output formatters. MvcOptions.OutputFormatters.Add(new HttpNoContentOutputFormatter()); MvcOptions.OutputFormatters.Add(new StringOutputFormatter()); - MvcOptions.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + MvcOptions.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); // Set up default mapping for json extensions to content type MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat( diff --git a/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs b/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs index 0467d0e51c..8990b1cb9c 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs @@ -76,6 +76,71 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } } + [Theory] + [MemberData(nameof(WriteCorrectCharacterEncoding))] + public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding( + string content, + string encodingAsString, + bool isDefaultEncoding) + { + // Arrange + var formatter = GetOutputFormatter(); + var expectedContent = "\"" + content + "\""; + var mediaType = MediaTypeHeaderValue.Parse(string.Format("application/json; charset={0}", encodingAsString)); + var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding); + + + var body = new MemoryStream(); + var actionContext = GetActionContext(mediaType, body); + + var outputFormatterContext = new OutputFormatterWriteContext( + actionContext.HttpContext, + new TestHttpResponseStreamWriterFactory().CreateWriter, + typeof(string), + content) + { + ContentType = new StringSegment(mediaType.ToString()), + }; + + // Act + await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString)); + + // Assert + var actualContent = encoding.GetString(body.ToArray()); + Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task WriteResponseBodyAsync_Encodes() + { + // Arrange + var formatter = GetOutputFormatter(); + var expectedContent = "{\"key\":\"Hello \\n Wörld\"}"; + var content = new { key = "Hello \n Wörld" }; + + var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); + var encoding = CreateOrGetSupportedEncoding(formatter, "utf-8", isDefaultEncoding: true); + + var body = new MemoryStream(); + var actionContext = GetActionContext(mediaType, body); + + var outputFormatterContext = new OutputFormatterWriteContext( + actionContext.HttpContext, + new TestHttpResponseStreamWriterFactory().CreateWriter, + typeof(string), + content) + { + ContentType = new StringSegment(mediaType.ToString()), + }; + + // Act + await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8")); + + // Assert + var actualContent = encoding.GetString(body.ToArray()); + Assert.Equal(expectedContent, actualContent); + } + [Fact] public async Task ErrorDuringSerialization_DoesNotCloseTheBrackets() { diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs index 0a3d90e18b..cd245872ef 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs @@ -1,56 +1,13 @@ // 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; -using System.IO; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using Xunit; - namespace Microsoft.AspNetCore.Mvc.Formatters { public class SystemTextJsonOutputFormatterTest : JsonOutputFormatterTestBase { protected override TextOutputFormatter GetOutputFormatter() { - return new SystemTextJsonOutputFormatter(new JsonOptions()); - } - - [Theory] - [MemberData(nameof(WriteCorrectCharacterEncoding))] - public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding( - string content, - string encodingAsString, - bool isDefaultEncoding) - { - // Arrange - var formatter = GetOutputFormatter(); - var expectedContent = "\"" + JavaScriptEncoder.Default.Encode(content) + "\""; - var mediaType = MediaTypeHeaderValue.Parse(string.Format("application/json; charset={0}", encodingAsString)); - var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding); - - - var body = new MemoryStream(); - var actionContext = GetActionContext(mediaType, body); - - var outputFormatterContext = new OutputFormatterWriteContext( - actionContext.HttpContext, - new TestHttpResponseStreamWriterFactory().CreateWriter, - typeof(string), - content) - { - ContentType = new StringSegment(mediaType.ToString()), - }; - - // Act - await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString)); - - // Assert - var actualContent = encoding.GetString(body.ToArray()); - Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase); + return SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions()); } } } diff --git a/src/Mvc/Mvc.Core/test/HttpNotFoundObjectResultTest.cs b/src/Mvc/Mvc.Core/test/HttpNotFoundObjectResultTest.cs index 42ef48b2f0..7956120d2a 100644 --- a/src/Mvc/Mvc.Core/test/HttpNotFoundObjectResultTest.cs +++ b/src/Mvc/Mvc.Core/test/HttpNotFoundObjectResultTest.cs @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/HttpOkObjectResultTest.cs b/src/Mvc/Mvc.Core/test/HttpOkObjectResultTest.cs index 49fac14fff..8735a8c89f 100644 --- a/src/Mvc/Mvc.Core/test/HttpOkObjectResultTest.cs +++ b/src/Mvc/Mvc.Core/test/HttpOkObjectResultTest.cs @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs index df59a73215..a615d92675 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs @@ -2,6 +2,7 @@ // 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.Text.Encodings.Web; using System.Text.Json; using Microsoft.AspNetCore.Html; using Microsoft.Extensions.Options; @@ -10,11 +11,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { internal class SystemTextJsonHelper : IJsonHelper { - private readonly JsonOptions _options; + private readonly JsonSerializerOptions _htmlSafeJsonSerializerOptions; public SystemTextJsonHelper(IOptions options) { - _options = options.Value; + _htmlSafeJsonSerializerOptions = GetHtmlSafeSerializerOptions(options.Value.JsonSerializerOptions); } /// @@ -22,8 +23,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { // JsonSerializer always encodes non-ASCII chars, so we do not need // to do anything special with the SerializerOptions - var json = JsonSerializer.Serialize(value, _options.JsonSerializerOptions); + var json = JsonSerializer.Serialize(value, _htmlSafeJsonSerializerOptions); return new HtmlString(json); } + + private static JsonSerializerOptions GetHtmlSafeSerializerOptions(JsonSerializerOptions serializerOptions) + { + if (serializerOptions.Encoder is null || serializerOptions.Encoder == JavaScriptEncoder.Default) + { + return serializerOptions; + } + + return serializerOptions.Copy(JavaScriptEncoder.Default); + } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Rendering/JsonHelperTestBase.cs b/src/Mvc/Mvc.ViewFeatures/test/Rendering/JsonHelperTestBase.cs index cfbabe735b..205a3479b6 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Rendering/JsonHelperTestBase.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Rendering/JsonHelperTestBase.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering // Assert var htmlString = Assert.IsType(result); - Assert.Equal(expectedOutput, htmlString.ToString()); + Assert.Equal(expectedOutput, htmlString.ToString(), ignoreCase: true); } [Fact] @@ -71,14 +71,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { HTML = $"Hello pingüino" }; - var expectedOutput = "{\"html\":\"Hello ping\\u00fcino\"}"; + var expectedOutput = "{\"html\":\"Hello pingüino\"}"; // Act var result = helper.Serialize(obj); // Assert var htmlString = Assert.IsType(result); - Assert.Equal(expectedOutput, htmlString.ToString()); + Assert.Equal(expectedOutput, htmlString.ToString(), ignoreCase: true); } [Fact] @@ -99,5 +99,25 @@ namespace Microsoft.AspNetCore.Mvc.Rendering var htmlString = Assert.IsType(result); Assert.Equal(expectedOutput, htmlString.ToString()); } + + + [Fact] + public virtual void Serialize_WithHTMLNonAsciiAndControlChars() + { + // Arrange + var helper = GetJsonHelper(); + var obj = new + { + HTML = "Hello \n pingüino" + }; + var expectedOutput = "{\"html\":\"\\u003cb\\u003eHello \\n pingüino\\u003c/b\\u003e\"}"; + + // Act + var result = helper.Serialize(obj); + + // Assert + var htmlString = Assert.IsType(result); + Assert.Equal(expectedOutput, htmlString.ToString()); + } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Rendering/SystemTextJsonHelperTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Rendering/SystemTextJsonHelperTest.cs index ce03e5f633..e1174f360b 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Rendering/SystemTextJsonHelperTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Rendering/SystemTextJsonHelperTest.cs @@ -1,7 +1,7 @@ // 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.Text.Json; +using System.Text.Encodings.Web; using Microsoft.AspNetCore.Html; using Microsoft.Extensions.Options; using Xunit; @@ -10,31 +10,13 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { public class SystemTextJsonHelperTest : JsonHelperTestBase { - protected override IJsonHelper GetJsonHelper() + protected override IJsonHelper GetJsonHelper() => GetJsonHelper(new JsonOptions()); + + private static IJsonHelper GetJsonHelper(JsonOptions options) { - var options = new JsonOptions() { JsonSerializerOptions = { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } }; return new SystemTextJsonHelper(Options.Create(options)); } - [Fact] - public override void Serialize_EscapesHtmlByDefault() - { - // Arrange - var helper = GetJsonHelper(); - var obj = new - { - HTML = "John Doe" - }; - var expectedOutput = "{\"html\":\"\\u003Cb\\u003EJohn Doe\\u003C/b\\u003E\"}"; - - // Act - var result = helper.Serialize(obj); - - // Assert - var htmlString = Assert.IsType(result); - Assert.Equal(expectedOutput, htmlString.ToString()); - } - [Fact] public override void Serialize_WithNonAsciiChars() { @@ -53,5 +35,56 @@ namespace Microsoft.AspNetCore.Mvc.Rendering var htmlString = Assert.IsType(result); Assert.Equal(expectedOutput, htmlString.ToString()); } + + [Fact] + public override void Serialize_WithHTMLNonAsciiAndControlChars() + { + // Arrange + var helper = GetJsonHelper(); + var obj = new + { + HTML = "Hello \n pingüino" + }; + var expectedOutput = "{\"html\":\"\\u003Cb\\u003EHello \\n ping\\u00FCino\\u003C/b\\u003E\"}"; + + // Act + var result = helper.Serialize(obj); + + // Assert + var htmlString = Assert.IsType(result); + Assert.Equal(expectedOutput, htmlString.ToString()); + } + + [Fact] + public void Serialize_UsesOptionsConfiguredInTheProvider() + { + // Arrange + // This should use property-casing and indentation, but the result should be HTML-safe + var options = new JsonOptions + { + JsonSerializerOptions = + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + PropertyNamingPolicy = null, + WriteIndented = true, + } + }; + var helper = GetJsonHelper(options); + var obj = new + { + HTML = "John" + }; + var expectedOutput = +@"{ + ""HTML"": ""\u003Cb\u003EJohn\u003C/b\u003E"" +}"; + + // Act + var result = helper.Serialize(obj); + + // Assert + var htmlString = Assert.IsType(result); + Assert.Equal(expectedOutput, htmlString.ToString()); + } } } diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs index c4f7ee9dcc..c5c4ae51fd 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; @@ -11,6 +10,7 @@ using System.Text; using System.Threading.Tasks; using FormatterWebSite.Controllers; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -21,13 +21,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { protected JsonOutputFormatterTestBase(MvcTestFixture fixture) { - var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); - Client = factory.CreateDefaultClient(); + Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); + Client = Factory.CreateDefaultClient(); } private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup(); + public WebApplicationFactory Factory { get; } public HttpClient Client { get; } [Fact] @@ -100,6 +101,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("\"Hello Mr. 🦊\"", await response.Content.ReadAsStringAsync()); } + [Fact] + public virtual async Task Formatting_StringValueWithNonAsciiCharacters() + { + // Act + var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.StringWithNonAsciiContent)}"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + Assert.Equal("\"Une bête de cirque\"", await response.Content.ReadAsStringAsync()); + } + [Fact] public virtual async Task Formatting_SimpleModel() { diff --git a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs index d508bfe731..d0af74ada1 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Net; +using System.Text.Encodings.Web; using System.Threading.Tasks; using FormatterWebSite.Controllers; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -15,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/11459")] + [Fact] public override Task SerializableErrorIsReturnedInExpectedFormat() => base.SerializableErrorIsReturnedInExpectedFormat(); [Fact] @@ -29,6 +31,25 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("\"Hello Mr. \\uD83E\\uDD8A\"", await response.Content.ReadAsStringAsync()); } + [Fact] + public async Task Formatting_WithCustomEncoder() + { + // Arrange + static void ConfigureServices(IServiceCollection serviceCollection) + { + serviceCollection.AddControllers() + .AddJsonOptions(o => o.JsonSerializerOptions.Encoder = JavaScriptEncoder.Default); + } + var client = Factory.WithWebHostBuilder(c => c.ConfigureServices(ConfigureServices)).CreateClient(); + + // Act + var response = await client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.StringWithNonAsciiContent)}"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + Assert.Equal("\"Une b\\u00EAte de cirque\"", await response.Content.ReadAsStringAsync()); + } + [Fact] public override Task Formatting_DictionaryType() => base.Formatting_DictionaryType(); diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs index bf8b004553..67ae87d482 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs @@ -20,6 +20,9 @@ namespace FormatterWebSite.Controllers [HttpGet] public ActionResult StringWithUnicodeResult() => "Hello Mr. 🦊"; + [HttpGet] + public ActionResult StringWithNonAsciiContent() => "Une bête de cirque"; + [HttpGet] public ActionResult SimpleModelResult() => new SimpleModel { Id = 10, Name = "Test", StreetName = "Some street" };