diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs index 445a265592..e5b254e1e1 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs @@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Razor.Runtime return GetString("TagHelperDescriptorFactory_Tag"); } + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'. + /// + internal static string TagHelperDescriptorFactory_InvalidBoundAttributeName + { + get { return GetString("TagHelperDescriptorFactory_InvalidBoundAttributeName"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidBoundAttributeName(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidBoundAttributeName"), p0, p1, p2); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx index bb3fdbd8ae..19695614ab 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx +++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx @@ -153,4 +153,7 @@ Tag + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs index 627d63f7a2..6eb7ddeae8 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// public static class TagHelperDescriptorFactory { + private const string DataDashPrefix = "data-"; private const string TagHelperNameEnding = "TagHelper"; private const string HtmlCaseRegexReplacement = "-$1$2"; @@ -44,7 +45,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers [NotNull] ErrorSink errorSink) { var typeInfo = type.GetTypeInfo(); - var attributeDescriptors = GetAttributeDescriptors(type); + var attributeDescriptors = GetAttributeDescriptors(type, errorSink); var targetElementAttributes = GetValidTargetElementAttributes(typeInfo, errorSink); var tagHelperDescriptors = BuildTagHelperDescriptors( @@ -201,14 +202,47 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers return validName; } - private static IEnumerable GetAttributeDescriptors(Type type) + private static IEnumerable GetAttributeDescriptors( + Type type, + ErrorSink errorSink) { - var properties = type.GetRuntimeProperties().Where(IsAccessibleProperty); - var attributeDescriptors = properties.Select(ToAttributeDescriptor); + var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty); + var attributeDescriptors = new List(); + + foreach (var property in accessibleProperties) + { + var descriptor = ToAttributeDescriptor(property); + if (ValidateTagHelperAttributeDescriptor(descriptor, type, errorSink)) + { + attributeDescriptors.Add(descriptor); + } + } return attributeDescriptors; } + private static bool ValidateTagHelperAttributeDescriptor( + TagHelperAttributeDescriptor attributeDescriptor, + Type parentType, + ErrorSink errorSink) + { + // data-* attributes are explicitly not implemented by user agents and are not intended for use on + // the server; therefore it's invalid for TagHelpers to bind to them. + if (attributeDescriptor.Name.StartsWith(DataDashPrefix, StringComparison.OrdinalIgnoreCase)) + { + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidBoundAttributeName( + attributeDescriptor.PropertyName, + parentType.FullName, + DataDashPrefix)); + + return false; + } + + return true; + } + private static TagHelperAttributeDescriptor ToAttributeDescriptor(PropertyInfo property) { var attributeNameAttribute = property.GetCustomAttribute(inherit: false); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs new file mode 100644 index 0000000000..e27730e084 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.Internal.Web.Utils; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + public class CaseSensitiveTagHelperAttributeDescriptorComparer : IEqualityComparer + { + public static readonly CaseSensitiveTagHelperAttributeDescriptorComparer Default = + new CaseSensitiveTagHelperAttributeDescriptorComparer(); + + private CaseSensitiveTagHelperAttributeDescriptorComparer() + { + } + + public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) + { + return + // Normal comparer doesn't care about case, in tests we do. + string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal) && + string.Equals(descriptorX.PropertyName, descriptorY.PropertyName, StringComparison.Ordinal) && + string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal); + } + + public int GetHashCode(TagHelperAttributeDescriptor descriptor) + { + return HashCodeCombiner + .Start() + .Add(descriptor.Name, StringComparer.Ordinal) + .Add(descriptor.PropertyName, StringComparer.Ordinal) + .Add(descriptor.TypeName, StringComparer.Ordinal) + .CombinedHash; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperDescriptorComparer.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperDescriptorComparer.cs index 6bc5acdbab..1096ef93c5 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperDescriptorComparer.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperDescriptorComparer.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } bool IEqualityComparer.Equals( - TagHelperDescriptor descriptorX, + TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) { return base.Equals(descriptorX, descriptorY) && @@ -33,7 +33,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers StringComparer.Ordinal) && descriptorX.Attributes.SequenceEqual( descriptorY.Attributes, - CaseSensitiveAttributeDescriptorComparer.Default); + CaseSensitiveTagHelperAttributeDescriptorComparer.Default); } int IEqualityComparer.GetHashCode(TagHelperDescriptor descriptor) @@ -51,39 +51,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers foreach (var attribute in descriptor.Attributes) { - hashCodeCombiner.Add(CaseSensitiveAttributeDescriptorComparer.Default.GetHashCode(attribute)); + hashCodeCombiner.Add(CaseSensitiveTagHelperAttributeDescriptorComparer.Default.GetHashCode(attribute)); } return hashCodeCombiner.CombinedHash; } - - private class CaseSensitiveAttributeDescriptorComparer : IEqualityComparer - { - public static readonly CaseSensitiveAttributeDescriptorComparer Default = - new CaseSensitiveAttributeDescriptorComparer(); - - private CaseSensitiveAttributeDescriptorComparer() - { - } - - public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) - { - return - // Normal comparer doesn't care about case, in tests we do. - string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal) && - string.Equals(descriptorX.PropertyName, descriptorY.PropertyName, StringComparison.Ordinal) && - string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal); - } - - public int GetHashCode(TagHelperAttributeDescriptor descriptor) - { - return HashCodeCombiner - .Start() - .Add(descriptor.Name, StringComparer.Ordinal) - .Add(descriptor.PropertyName, StringComparer.Ordinal) - .Add(descriptor.TypeName, StringComparer.Ordinal) - .CombinedHash; - } - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index 859f134902..f6b29b5947 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -861,6 +861,107 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers Assert.Empty(result); } + public static TheoryData InvalidTagHelperAttributeDescriptorData + { + get + { + var errorFormat = "Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML " + + "attributes beginning with 'data-'."; + + // type, expectedAttributeDescriptors, expectedErrors + return new TheoryData, string[]> + { + { + typeof(InvalidBoundAttribute), + Enumerable.Empty(), + new[] { + string.Format( + errorFormat, + nameof(InvalidBoundAttribute.DataSomething), + typeof(InvalidBoundAttribute).FullName) + } + }, + { + typeof(InvalidBoundAttributeWithValid), + new[] { + new TagHelperAttributeDescriptor( + "int-attribute", + typeof(InvalidBoundAttributeWithValid) + .GetProperty(nameof(InvalidBoundAttributeWithValid.IntAttribute))) + }, + new[] { + string.Format( + errorFormat, + nameof(InvalidBoundAttributeWithValid.DataSomething), + typeof(InvalidBoundAttributeWithValid).FullName) + } + }, + { + typeof(OverriddenInvalidBoundAttributeWithValid), + new[] { + new TagHelperAttributeDescriptor( + "valid-something", + typeof(OverriddenInvalidBoundAttributeWithValid) + .GetProperty(nameof(OverriddenInvalidBoundAttributeWithValid.DataSomething))) + }, + new string[0] + }, + { + typeof(OverriddenValidBoundAttributeWithInvalid), + Enumerable.Empty(), + new[] { + string.Format( + errorFormat, + nameof(OverriddenValidBoundAttributeWithInvalid.ValidSomething), + typeof(OverriddenValidBoundAttributeWithInvalid).FullName) + } + }, + { + typeof(OverriddenValidBoundAttributeWithInvalidUpperCase), + Enumerable.Empty(), + new[] { + string.Format( + errorFormat, + nameof(OverriddenValidBoundAttributeWithInvalidUpperCase.ValidSomething), + typeof(OverriddenValidBoundAttributeWithInvalidUpperCase).FullName) + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(InvalidTagHelperAttributeDescriptorData))] + public void CreateDescriptor_DoesNotAllowDataDashAttributes( + Type type, + IEnumerable expectedAttributeDescriptors, + string[] expectedErrors) + { + // Arrange + var errorSink = new ErrorSink(); + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, type, errorSink); + + // Assert + var actualErrors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrors.Length, actualErrors.Length); + + for (var i = 0; i < actualErrors.Length; i++) + { + var actualError = actualErrors[i]; + Assert.Equal(1, actualError.Length); + Assert.Equal(SourceLocation.Zero, actualError.Location); + Assert.Equal(expectedErrors[i], actualError.Message); + } + + var actualDescriptor = Assert.Single(descriptors); + Assert.Equal( + expectedAttributeDescriptors, + actualDescriptor.Attributes, + CaseSensitiveTagHelperAttributeDescriptorComparer.Default); + } + [TargetElement(Attributes = "class")] private class AttributeTargetingTagHelper : TagHelper { @@ -999,7 +1100,34 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private class UNSuffixedCLASS : TagHelper { public int UNSuffixedATTRIBUTE { get; set; } + } + private class InvalidBoundAttribute : TagHelper + { + public string DataSomething { get; set; } + } + + private class InvalidBoundAttributeWithValid : SingleAttributeTagHelper + { + public string DataSomething { get; set; } + } + + private class OverriddenInvalidBoundAttributeWithValid : TagHelper + { + [HtmlAttributeName("valid-something")] + public string DataSomething { get; set; } + } + + private class OverriddenValidBoundAttributeWithInvalid : TagHelper + { + [HtmlAttributeName("data-something")] + public string ValidSomething { get; set; } + } + + private class OverriddenValidBoundAttributeWithInvalidUpperCase : TagHelper + { + [HtmlAttributeName("DATA-SOMETHING")] + public string ValidSomething { get; set; } } } } \ No newline at end of file