diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonOutputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonOutputFormatter.cs index 8ecdebb2e7..744d558680 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonOutputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonOutputFormatter.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.ComponentModel; using System.IO; using System.Text; using System.Threading.Tasks; @@ -62,6 +63,16 @@ public JsonOutputFormatter(JsonSerializerSettings serializerSettings, ArrayPool< /// protected JsonSerializerSettings SerializerSettings { get; } + /// + /// Gets the used to configure the . + /// + /// + /// Any modifications to the object after this + /// has been used will have no effect. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public JsonSerializerSettings PublicSerializerSettings => SerializerSettings; + /// /// Writes the given as JSON using the given /// . diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/JsonHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/JsonHelper.cs index 4fcdfc3579..bda3a50784 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/JsonHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/JsonHelper.cs @@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// public class JsonHelper : IJsonHelper { + private const string AllowJsonHtml = "Switch.Microsoft.AspNetCore.Mvc.AllowJsonHtml"; private readonly JsonOutputFormatter _jsonOutputFormatter; private readonly ArrayPool _charPool; @@ -46,7 +47,15 @@ public JsonHelper(JsonOutputFormatter jsonOutputFormatter, ArrayPool charP /// public IHtmlContent Serialize(object value) { - return SerializeInternal(_jsonOutputFormatter, value); + if (AppContext.TryGetSwitch(AllowJsonHtml, out var allowJsonHtml) && allowJsonHtml) + { + return SerializeInternal(_jsonOutputFormatter, value); + } + + var settings = ShallowCopy(_jsonOutputFormatter.PublicSerializerSettings); + settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml; + + return Serialize(value, settings); } /// @@ -69,5 +78,42 @@ private IHtmlContent SerializeInternal(JsonOutputFormatter jsonOutputFormatter, return new HtmlString(stringWriter.ToString()); } + + private static JsonSerializerSettings ShallowCopy(JsonSerializerSettings settings) + { + var copiedSettings = new JsonSerializerSettings + { + FloatParseHandling = settings.FloatParseHandling, + FloatFormatHandling = settings.FloatFormatHandling, + DateParseHandling = settings.DateParseHandling, + DateTimeZoneHandling = settings.DateTimeZoneHandling, + DateFormatHandling = settings.DateFormatHandling, + Formatting = settings.Formatting, + MaxDepth = settings.MaxDepth, + DateFormatString = settings.DateFormatString, + Context = settings.Context, + Error = settings.Error, + SerializationBinder = settings.SerializationBinder, + TraceWriter = settings.TraceWriter, + Culture = settings.Culture, + ReferenceResolverProvider = settings.ReferenceResolverProvider, + EqualityComparer = settings.EqualityComparer, + ContractResolver = settings.ContractResolver, + ConstructorHandling = settings.ConstructorHandling, + TypeNameAssemblyFormatHandling = settings.TypeNameAssemblyFormatHandling, + MetadataPropertyHandling = settings.MetadataPropertyHandling, + TypeNameHandling = settings.TypeNameHandling, + PreserveReferencesHandling = settings.PreserveReferencesHandling, + Converters = settings.Converters, + DefaultValueHandling = settings.DefaultValueHandling, + NullValueHandling = settings.NullValueHandling, + ObjectCreationHandling = settings.ObjectCreationHandling, + MissingMemberHandling = settings.MissingMemberHandling, + ReferenceLoopHandling = settings.ReferenceLoopHandling, + CheckAdditionalContent = settings.CheckAdditionalContent, + }; + + return copiedSettings; + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs index 60697caf63..21829b3937 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs @@ -245,12 +245,10 @@ public async Task ActionWithRequireHttps_AllowsHttpsRequests(string method) public async Task JsonHelper_RendersJson_WithCamelCaseNames() { // Arrange - var json = "{\"id\":9000,\"fullName\":\"John Smith\"}"; - var expectedBody = string.Format( - @"", - json); + var expectedBody = +@""; // Act var response = await Client.GetAsync("Home/JsonHelperInView"); diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/JsonHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/JsonHelperTest.cs new file mode 100644 index 0000000000..bcc3e4b538 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/JsonHelperTest.cs @@ -0,0 +1,68 @@ +// 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.Buffers; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Formatters; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + public class JsonHelperTest + { + [Fact] + public void Serialize_EscapesHtmlByDefault() + { + // Arrange + var settings = new JsonSerializerSettings() + { + StringEscapeHandling = StringEscapeHandling.EscapeNonAscii, + }; + var helper = new JsonHelper( + new JsonOutputFormatter(settings, ArrayPool.Shared), + ArrayPool.Shared); + 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 void Serialize_MaintainsSettingsAndEscapesHtml() + { + // Arrange + var settings = new JsonSerializerSettings() + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy(), + }, + }; + var helper = new JsonHelper( + new JsonOutputFormatter(settings, ArrayPool.Shared), + ArrayPool.Shared); + var obj = new + { + FullHtml = "John Doe" + }; + var expectedOutput = "{\"fullHtml\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}"; + + // Act + var result = helper.Serialize(obj); + + // Assert + var htmlString = Assert.IsType(result); + Assert.Equal(expectedOutput, htmlString.ToString()); + } + } +}