diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultDisplayTemplates.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultDisplayTemplates.cs index ba04cf35a1..03e2b32be6 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultDisplayTemplates.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultDisplayTemplates.cs @@ -213,7 +213,13 @@ namespace Microsoft.AspNet.Mvc.Rendering if (templateInfo.TemplateDepth > 1) { - return modelMetadata.Model == null ? modelMetadata.NullDisplayText : modelMetadata.SimpleDisplayText; + var text = modelMetadata.SimpleDisplayText; + if (modelMetadata.HtmlEncode) + { + text = html.Encode(text); + } + + return text; } var serviceProvider = html.ViewContext.HttpContext.RequestServices; diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs index 848fda53ae..742414c807 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs @@ -232,7 +232,18 @@ namespace Microsoft.AspNet.Mvc.Rendering if (templateInfo.TemplateDepth > 1) { - return modelMetadata.Model == null ? modelMetadata.NullDisplayText : modelMetadata.SimpleDisplayText; + if (modelMetadata.Model == null) + { + return modelMetadata.NullDisplayText; + } + + var text = modelMetadata.SimpleDisplayText; + if (modelMetadata.HtmlEncode) + { + text = html.Encode(text); + } + + return text; } var serviceProvider = html.ViewContext.HttpContext.RequestServices; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs index b5d292cf05..c21d4fd24a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs @@ -205,6 +205,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return base.ComputeHideSurroundingHtml(); } + /// + /// Calculate based on presence of a + /// and its value. + /// + /// + /// Calculated value. false if a + /// exists and its value + /// is false. true otherwise. + /// + protected override bool ComputeHtmlEncode() + { + if (PrototypeCache.DisplayFormat != null) + { + return PrototypeCache.DisplayFormat.HtmlEncode; + } + + return base.ComputeHtmlEncode(); + } + protected override bool ComputeIsReadOnly() { if (PrototypeCache.Editable != null) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs index 130c6bf53c..257c278cb0 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private string _editFormatString; private bool _hasNonDefaultEditFormat; private bool _hideSurroundingHtml; + private bool _htmlEncode; private bool _isReadOnly; private bool _isComplexType; private bool _isRequired; @@ -39,6 +40,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private bool _editFormatStringComputed; private bool _hasNonDefaultEditFormatComputed; private bool _hideSurroundingHtmlComputed; + private bool _htmlEncodeComputed; private bool _isReadOnlyComputed; private bool _isComplexTypeComputed; private bool _isRequiredComputed; @@ -252,6 +254,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + /// + public sealed override bool HtmlEncode + { + get + { + if (!_htmlEncodeComputed) + { + _htmlEncode = ComputeHtmlEncode(); + _htmlEncodeComputed = true; + } + + return _htmlEncode; + } + + set + { + _htmlEncode = value; + _htmlEncodeComputed = true; + } + } + public sealed override bool IsReadOnly { get @@ -419,6 +442,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return base.HideSurroundingHtml; } + /// + /// Calculate the value. + /// + /// Calculated value. + protected virtual bool ComputeHtmlEncode() + { + return base.HtmlEncode; + } + protected virtual bool ComputeIsReadOnly() { return base.IsReadOnly; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs index 39c54f0b0e..93cab06edb 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs @@ -19,17 +19,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private EfficientTypePropertyKey _cacheKey; // Backing fields for virtual properties with default values. - private bool _convertEmptyStringToNull; - private bool _isRequired; + private bool _convertEmptyStringToNull = true; + private bool _htmlEncode = true; + private bool _showForDisplay = true; + private bool _showForEdit = true; private object _model; private Func _modelAccessor; private int _order = DefaultOrder; + private bool _isRequired; private IEnumerable _properties; private Type _realModelType; private string _simpleDisplayText; - private bool _showForDisplay = true; - private bool _showForEdit = true; public ModelMetadata([NotNull] IModelMetadataProvider provider, Type containerType, @@ -43,7 +44,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding _modelAccessor = modelAccessor; _modelType = modelType; _propertyName = propertyName; - _convertEmptyStringToNull = true; _isRequired = !modelType.AllowsNullValue(); } @@ -111,6 +111,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public virtual bool HasNonDefaultEditFormat { get; set; } + /// + /// Gets or sets a value indicating whether the value should be HTML-encoded. + /// + /// If true, value should be HTML-encoded. Default is true. + public virtual bool HtmlEncode + { + get { return _htmlEncode; } + set { _htmlEncode = value; } + } + /// /// Gets or sets a value indicating whether the "HiddenInput" display template should return /// string.Empty (not the expression value) and whether the "HiddenInput" editor template should not diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultDisplayTemplatesTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultDisplayTemplatesTests.cs index 98e762f599..2f57c32e69 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultDisplayTemplatesTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultDisplayTemplatesTests.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; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.ModelBinding; @@ -14,6 +13,33 @@ namespace Microsoft.AspNet.Mvc.Core { public class DefaultDisplayTemplateTests { + // Input value; HTML encode; expected value. + public static TheoryData HtmlEncodeData + { + get + { + return new TheoryData + { + { "Simple Display Text", false, "Simple Display Text" }, + { "Simple Display Text", true, "Simple Display Text" }, + { "text", false, "text" }, + { "text", true, "<blink>text</blink>" }, + { "&'\"", false, "&'\"" }, + { "&'\"", true, "&'"" }, + { " ¡ÿĀ", false, " ¡ÿĀ" }, // high ASCII + { " ¡ÿĀ", true, " ¡ÿĀ" }, + { "Chinese西雅图Chars", false, "Chinese西雅图Chars" }, + { "Chinese西雅图Chars", true, "Chinese西雅图Chars" }, + { "Unicode؃Format؃Char", false, "Unicode؃Format؃Char" }, // class Cf + { "Unicode؃Format؃Char", true, "Unicode؃Format؃Char" }, + { "UnicodeῼTitlecaseῼChar", false, "UnicodeῼTitlecaseῼChar" }, // class Lt + { "UnicodeῼTitlecaseῼChar", true, "UnicodeῼTitlecaseῼChar" }, + { "UnicodeःCombiningःChar", false, "UnicodeःCombiningःChar" }, // class Mc + { "UnicodeःCombiningःChar", true, "UnicodeःCombiningःChar" }, + }; + } + } + [Fact] public void ObjectTemplateDisplaysSimplePropertiesOnObjectByDefault() { @@ -54,8 +80,12 @@ namespace Microsoft.AspNet.Mvc.Core Assert.Equal(metadata.NullDisplayText, result); } - [Fact] - public void ObjectTemplateDisplaysSimpleDisplayTextWhenTemplateDepthGreaterThanOne() + [Theory] + [MemberData(nameof(HtmlEncodeData))] + public void ObjectTemplateDisplaysSimpleDisplayTextWhenTemplateDepthGreaterThanOne( + string simpleDisplayText, + bool htmlEncode, + string expectedResult) { // Arrange var model = new DefaultTemplatesUtilities.ObjectTemplateModel(); @@ -63,7 +93,8 @@ namespace Microsoft.AspNet.Mvc.Core var metadata = new EmptyModelMetadataProvider() .GetMetadataForType(() => model, typeof(DefaultTemplatesUtilities.ObjectTemplateModel)); - metadata.SimpleDisplayText = "Simple Display Text"; + metadata.HtmlEncode = htmlEncode; + metadata.SimpleDisplayText = simpleDisplayText; html.ViewData.ModelMetadata = metadata; html.ViewData.TemplateInfo.AddVisited("foo"); html.ViewData.TemplateInfo.AddVisited("bar"); @@ -72,7 +103,7 @@ namespace Microsoft.AspNet.Mvc.Core var result = DefaultDisplayTemplates.ObjectTemplate(html); // Assert - Assert.Equal(metadata.SimpleDisplayText, result); + Assert.Equal(expectedResult, result); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTests.cs index 419a3f40f1..f2d1982bfa 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTests.cs @@ -115,8 +115,12 @@ namespace Microsoft.AspNet.Mvc.Core Assert.Equal(metadata.NullDisplayText, result); } - [Fact] - public void ObjectTemplateDisplaysSimpleDisplayTextWithNonNullModelTemplateDepthGreaterThanOne() + [Theory] + [MemberData(nameof(DefaultDisplayTemplateTests.HtmlEncodeData), MemberType = typeof(DefaultDisplayTemplateTests))] + public void ObjectTemplateDisplaysSimpleDisplayTextWithNonNullModelTemplateDepthGreaterThanOne( + string simpleDisplayText, + bool htmlEncode, + string expectedResult) { // Arrange var model = new DefaultTemplatesUtilities.ObjectTemplateModel(); @@ -124,9 +128,10 @@ namespace Microsoft.AspNet.Mvc.Core var metadata = new EmptyModelMetadataProvider() .GetMetadataForType(() => model, typeof(DefaultTemplatesUtilities.ObjectTemplateModel)); - html.ViewData.ModelMetadata = metadata; + metadata.HtmlEncode = htmlEncode; metadata.NullDisplayText = "Null Display Text"; - metadata.SimpleDisplayText = "Simple Display Text"; + metadata.SimpleDisplayText = simpleDisplayText; + html.ViewData.ModelMetadata = metadata; html.ViewData.TemplateInfo.AddVisited("foo"); html.ViewData.TemplateInfo.AddVisited("bar"); @@ -134,7 +139,7 @@ namespace Microsoft.AspNet.Mvc.Core var result = DefaultEditorTemplates.ObjectTemplate(html); // Assert - Assert.Equal(metadata.SimpleDisplayText, result); + Assert.Equal(expectedResult, result); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs index d042fbdc0b..6d3e219c76 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs @@ -32,6 +32,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.True(metadata.ConvertEmptyStringToNull); Assert.False(metadata.HasNonDefaultEditFormat); Assert.False(metadata.HideSurroundingHtml); + Assert.True(metadata.HtmlEncode); Assert.True(metadata.IsComplexType); Assert.False(metadata.IsReadOnly); Assert.False(metadata.IsRequired); @@ -157,6 +158,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding metadata => metadata.HasNonDefaultEditFormat, true }, + { + new DisplayFormatAttribute { HtmlEncode = false }, + metadata => metadata.HtmlEncode, + false + }, + { + new DisplayFormatAttribute { HtmlEncode = true }, + metadata => metadata.HtmlEncode, + true + }, { new EditableAttribute(allowEdit: false), metadata => metadata.IsReadOnly, diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs index 385ff00ac6..ebf76df8e0 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { m => m.ConvertEmptyStringToNull = false, m => m.ConvertEmptyStringToNull, false }, { m => m.HasNonDefaultEditFormat = true, m => m.HasNonDefaultEditFormat, true }, { m => m.HideSurroundingHtml = true, m => m.HideSurroundingHtml, true }, + { m => m.HtmlEncode = false, m => m.HtmlEncode, false }, { m => m.IsReadOnly = true, m => m.IsReadOnly, true }, { m => m.IsRequired = true, m => m.IsRequired, true }, { m => m.ShowForDisplay = false, m => m.ShowForDisplay, false }, @@ -59,6 +60,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.True(metadata.ConvertEmptyStringToNull); Assert.False(metadata.HasNonDefaultEditFormat); Assert.False(metadata.HideSurroundingHtml); + Assert.True(metadata.HtmlEncode); Assert.False(metadata.IsComplexType); Assert.False(metadata.IsNullableValueType); Assert.False(metadata.IsReadOnly);