Create error when TagHelper binds to HTML attribute starting w/ data-.

- Added TagHelperDescriptorFactory tests to validate TagHelperAttributeDescriptor creation.

#342
This commit is contained in:
N. Taylor Mullen 2015-04-13 16:30:26 -07:00
parent 96dc7ea6c5
commit 5bcda94b2c
6 changed files with 227 additions and 36 deletions

View File

@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Razor.Runtime
return GetString("TagHelperDescriptorFactory_Tag");
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidBoundAttributeName
{
get { return GetString("TagHelperDescriptorFactory_InvalidBoundAttributeName"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'.
/// </summary>
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);

View File

@ -153,4 +153,7 @@
<data name="TagHelperDescriptorFactory_Tag" xml:space="preserve">
<value>Tag</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidBoundAttributeName" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'.</value>
</data>
</root>

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary>
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<TagHelperAttributeDescriptor> GetAttributeDescriptors(Type type)
private static IEnumerable<TagHelperAttributeDescriptor> 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<TagHelperAttributeDescriptor>();
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<HtmlAttributeNameAttribute>(inherit: false);

View File

@ -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<TagHelperAttributeDescriptor>
{
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;
}
}
}

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
}
bool IEqualityComparer<TagHelperDescriptor>.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<TagHelperDescriptor>.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<TagHelperAttributeDescriptor>
{
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;
}
}
}
}

View File

@ -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<Type, IEnumerable<TagHelperAttributeDescriptor>, string[]>
{
{
typeof(InvalidBoundAttribute),
Enumerable.Empty<TagHelperAttributeDescriptor>(),
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<TagHelperAttributeDescriptor>(),
new[] {
string.Format(
errorFormat,
nameof(OverriddenValidBoundAttributeWithInvalid.ValidSomething),
typeof(OverriddenValidBoundAttributeWithInvalid).FullName)
}
},
{
typeof(OverriddenValidBoundAttributeWithInvalidUpperCase),
Enumerable.Empty<TagHelperAttributeDescriptor>(),
new[] {
string.Format(
errorFormat,
nameof(OverriddenValidBoundAttributeWithInvalidUpperCase.ValidSomething),
typeof(OverriddenValidBoundAttributeWithInvalidUpperCase).FullName)
}
},
};
}
}
[Theory]
[MemberData(nameof(InvalidTagHelperAttributeDescriptorData))]
public void CreateDescriptor_DoesNotAllowDataDashAttributes(
Type type,
IEnumerable<TagHelperAttributeDescriptor> 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; }
}
}
}