Added FormTagHelperOptions for configuring default behavior of FormTagHelper:

- #1689
This commit is contained in:
damianedwards 2015-03-05 20:12:36 -08:00
parent a9feed4de1
commit 7c18e666a3
15 changed files with 363 additions and 14 deletions

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
@ -28,6 +29,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[Activate]
protected internal IHtmlGenerator Generator { get; set; }
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
[Activate]
protected internal IOptions<FormTagHelperOptions> Options { get; set; }
/// <summary>
/// The name of the action method.
/// </summary>
@ -108,7 +113,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
if (AntiForgery ?? antiForgeryDefault)
if (AntiForgery ?? Options.Options.GenerateAntiForgeryToken ?? antiForgeryDefault)
{
var antiForgeryTagBuilder = Generator.GenerateAntiForgery(ViewContext);
if (antiForgeryTagBuilder != null)

View File

@ -0,0 +1,21 @@
// 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.
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// Options pertaining to the default behavior of <see cref="FormTagHelper"/> instances.
/// </summary>
public class FormTagHelperOptions
{
/// <summary>
/// Whether the anti-forgery token should be generated by default for all instances of
/// <see cref="FormTagHelper"/>. Can be overridden on any given instance.
/// </summary>
/// <value>
/// Defaults to <c>null</c>, which indicates a token will only be generated if the <c>action</c>
/// attribute was not explicitly defined.
/// </value>
public bool? GenerateAntiForgeryToken { get; set; }
}
}

View File

@ -0,0 +1,18 @@
// 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 Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace Microsoft.Framework.DependencyInjection
{
/// <summary>
/// Used for adding options pertaining to <see cref="ITagHelper"/>s to an <see cref="IServiceCollection"/>.
/// </summary>
public interface ITagHelperOptionsCollection
{
/// <summary>
/// The <see cref="IServiceCollection"/>.
/// </summary>
IServiceCollection Services { get; }
}
}

View File

@ -0,0 +1,29 @@
// 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 Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// Used for adding options pertaining to <see cref="ITagHelper"/>s to an <see cref="IServiceCollection"/>.
/// </summary>
public class TagHelperOptionsCollection : ITagHelperOptionsCollection
{
/// <summary>
/// Creates a new <see cref="TagHelperOptionsCollection"/>;
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> instance to add the options to.</param>
public TagHelperOptionsCollection([NotNull] IServiceCollection serviceCollection)
{
Services = serviceCollection;
}
/// <summary>
/// The <see cref="IServiceCollection"/>.
/// </summary>
public IServiceCollection Services { get; }
}
}

View File

@ -0,0 +1,46 @@
// 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 Microsoft.AspNet.Mvc.TagHelpers;
using Microsoft.Framework.ConfigurationModel;
using Microsoft.Framework.Internal;
namespace Microsoft.Framework.DependencyInjection
{
/// <summary>
/// Extension methods for <see cref="ITagHelperOptionsCollection"/>.
/// </summary>
public static class TagHelperOptionsCollectionExtensions
{
/// <summary>
/// Configures options for the <see cref="FormTagHelper"/> from an <see cref="IConfiguration"/>.
/// </summary>
/// <param name="collection">The <see cref="ITagHelperOptionsCollection"/> instance this method extends.</param>
/// <param name="configuration">An <see cref="IConfiguration"/> to get the options from.</param>
/// <returns>The <see cref="ITagHelperOptionsCollection"/>.</returns>
public static ITagHelperOptionsCollection ConfigureForm(
[NotNull] this ITagHelperOptionsCollection collection,
[NotNull] IConfiguration configuration)
{
collection.Services.Configure<FormTagHelperOptions>(configuration);
return collection;
}
/// <summary>
/// Configures options for the <see cref="FormTagHelper"/> using a delegate.
/// </summary>
/// <param name="collection">The <see cref="ITagHelperOptionsCollection"/> instance this method extends.</param>
/// <param name="setupAction">The options setup delegate.</param>
/// <returns>The <see cref="ITagHelperOptionsCollection"/>.</returns>
public static ITagHelperOptionsCollection ConfigureForm(
[NotNull] this ITagHelperOptionsCollection collection,
[NotNull] Action<FormTagHelperOptions> setupAction)
{
collection.Services.Configure(setupAction);
return collection;
}
}
}

View File

@ -0,0 +1,27 @@
// 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 Microsoft.AspNet.Mvc.TagHelpers;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.Internal;
namespace Microsoft.Framework.DependencyInjection
{
/// <summary>
/// Extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class TagHelperServiceCollectionExtensions
{
/// <summary>
/// Creates an <see cref="ITagHelperOptionsCollection"/> which can be used to add options pertaining to
/// <see cref="ITagHelper"/>s to the <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> instance this method extends.</param>
/// <returns>The <see cref="ITagHelperOptionsCollection"/>.</returns>
public static ITagHelperOptionsCollection ConfigureTagHelpers(
[NotNull] this IServiceCollection serviceCollection)
{
return new TagHelperOptionsCollection(serviceCollection);
}
}
}

View File

@ -2,6 +2,7 @@
// 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.IO;
using System.Linq;
using System.Net.Http;
@ -13,15 +14,27 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public static string RetrieveAntiForgeryToken(string htmlContent, string actionUrl)
{
return RetrieveAntiForgeryTokens(
htmlContent,
attribute => attribute.Value.EndsWith(actionUrl, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
}
public static IEnumerable<string> RetrieveAntiForgeryTokens(
string htmlContent,
Func<XAttribute, bool> predicate = null)
{
predicate = predicate ?? (_ => true);
htmlContent = "<Root>" + htmlContent + "</Root>";
var reader = new StringReader(htmlContent);
var htmlDocument = XDocument.Load(reader);
foreach (var form in htmlDocument.Descendants("form"))
{
foreach (var attribute in form.Attributes())
{
if (string.Equals(attribute.Name.LocalName, "action", StringComparison.OrdinalIgnoreCase) &&
attribute.Value.EndsWith(actionUrl, StringComparison.OrdinalIgnoreCase))
if (string.Equals(attribute.Name.LocalName, "action", StringComparison.OrdinalIgnoreCase)
&& predicate(attribute))
{
foreach (var input in form.Descendants("input"))
{
@ -30,14 +43,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
input.Attribute("name").Value == "__RequestVerificationToken" &&
input.Attribute("type").Value == "hidden")
{
return input.Attributes("value").First().Value;
yield return input.Attributes("value").First().Value;
}
}
}
}
}
return null;
}
public static string RetrieveAntiForgeryCookie(HttpResponseMessage response)

View File

@ -0,0 +1,15 @@

<html>
<head>
<meta charset="utf-8" />
<title>Form</title>
</head>
<body>
<h2>Form Tag Helper Test</h2>
<form action="/MvcTagHelper_Home/Form" method="post"></form>
<form action="/MvcTagHelper_Home/Form" method="post"><input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
<form action="/MvcTagHelper_Home/Form" method="post"></form>
</body>
</html>

View File

@ -0,0 +1,15 @@

<html>
<head>
<meta charset="utf-8" />
<title>Form</title>
</head>
<body>
<h2>Form Tag Helper Test</h2>
<form action="/MvcTagHelper_Home/Form" method="post"><input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
<form action="/MvcTagHelper_Home/Form" method="post"><input name="__RequestVerificationToken" type="hidden" value="{1}" /></form>
<form action="/MvcTagHelper_Home/Form" method="post"></form>
</body>
</html>

View File

@ -0,0 +1,15 @@

<html>
<head>
<meta charset="utf-8" />
<title>Form</title>
</head>
<body>
<h2>Form Tag Helper Test</h2>
<form action="/MvcTagHelper_Home/Form" method="post"><input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
<form action="/MvcTagHelper_Home/Form" method="post"><input name="__RequestVerificationToken" type="hidden" value="{1}" /></form>
<form action="/MvcTagHelper_Home/Form" method="post"></form>
</body>
</html>

View File

@ -3,13 +3,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.TestHost;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal;
using MvcTagHelpersWebSite;
using Xunit;
@ -334,5 +339,41 @@ Products: Book1, Book2 (1)";
Products: Laptops (3)";
Assert.Equal(expected3, response4.Trim());
}
[Theory]
[InlineData(null)]
[InlineData(true)]
[InlineData(false)]
public async Task FormTagHelper_GeneratesExpectedContent(bool? optionsAntiForgery)
{
// Arrange
var newServices = new ServiceCollection();
newServices.ConfigureTagHelpers().ConfigureForm(options => options.GenerateAntiForgeryToken = optionsAntiForgery);
var serviceProvider = TestHelper.CreateServices("MvcTagHelpersWebSite", newServices);
var server = TestServer.Create(serviceProvider, _app);
var client = server.CreateClient();
var expectedMediaType = MediaTypeHeaderValue.Parse("text/html; charset=utf-8");
// The K runtime compiles every file under compiler/resources as a resource at runtime with the same name
// as the file name, in order to update a baseline you just need to change the file in that folder.
var resourceName = string.Format(
"compiler/resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.{0}.html",
optionsAntiForgery?.ToString() ?? "null"
);
var expectedContent = await _resourcesAssembly.ReadResourceAsStringAsync(resourceName);
// Act
// The host is not important as everything runs in memory and tests are isolated from each other.
var response = await client.GetAsync("http://localhost/MvcTagHelper_Home/Form");
var responseContent = await response.Content.ReadAsStringAsync();
var forgeryTokens = AntiForgeryTestHelper.RetrieveAntiForgeryTokens(responseContent);
expectedContent = string.Format(expectedContent, forgeryTokens.ToArray());
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedMediaType, response.Content.Headers.ContentType);
Assert.Equal(expectedContent.Trim(), responseContent.Trim());
}
}
}

View File

@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.WebEncoders;
using Moq;
using Xunit;
@ -89,12 +90,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
[Theory]
[InlineData(true, "<input />")]
[InlineData(false, "")]
[InlineData(null, "<input />")]
public async Task ProcessAsync_GeneratesAntiForgeryCorrectly(bool? antiForgery, string expectedPostContent)
[InlineData(null, true, "<input />")]
[InlineData(null, false, "")]
[InlineData(null, null, "<input />")]
[InlineData(true, true, "<input />")]
[InlineData(true, false, "")]
[InlineData(true, null, "<input />")]
[InlineData(false, true, "<input />")]
[InlineData(false, false, "")]
[InlineData(false, null, "")]
public async Task ProcessAsync_GeneratesAntiForgeryCorrectly(
bool? optionsAntiForgery,
bool? antiForgery,
string expectedPostContent)
{
// Arrange
var options = MakeOptions(new FormTagHelperOptions { GenerateAntiForgeryToken = optionsAntiForgery });
var viewContext = CreateViewContext();
var context = new TagHelperContext(
allAttributes: new Dictionary<string, object>(),
@ -128,6 +139,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Action = "Index",
AntiForgery = antiForgery,
Generator = generator.Object,
Options = options,
ViewContext = viewContext,
};
@ -268,9 +280,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public async Task ProcessAsync_RestoresBoundAttributesIfActionIsSpecified(string htmlAction)
{
// Arrange
var options = MakeOptions<FormTagHelperOptions>();
var formTagHelper = new FormTagHelper
{
Method = "POST"
Method = "POST",
Options = options
};
var output = new TagHelperOutput("form",
attributes: new Dictionary<string, string>
@ -309,14 +323,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
[Theory]
[InlineData(true, "<input />")]
[InlineData(false, "")]
[InlineData(null, "")]
[InlineData(null, true, "<input />")]
[InlineData(null, false, "")]
[InlineData(null, null, "")]
[InlineData(true, true, "<input />")]
[InlineData(true, false, "")]
[InlineData(true, null, "<input />")]
[InlineData(false, true, "<input />")]
[InlineData(false, false, "")]
[InlineData(false, null, "")]
public async Task ProcessAsync_SupportsAntiForgeryIfActionIsSpecified(
bool? optionsAntiForgery,
bool? antiForgery,
string expectedPostContent)
{
// Arrange
var options = MakeOptions(new FormTagHelperOptions { GenerateAntiForgeryToken = optionsAntiForgery });
var viewContext = CreateViewContext();
var generator = new Mock<IHtmlGenerator>();
@ -326,6 +348,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
AntiForgery = antiForgery,
Generator = generator.Object,
Options = options,
ViewContext = viewContext,
};
@ -398,6 +421,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedErrorMessage, ex.Message);
}
private static IOptions<TOptions> MakeOptions<TOptions>(TOptions options = null)
where TOptions : class, new()
{
var optionsAccessor = new Mock<IOptions<TOptions>>();
optionsAccessor.Setup(o => o.Options).Returns(options ?? new TOptions());
return optionsAccessor.Object;
}
private static ViewContext CreateViewContext()
{
var actionContext = new ActionContext(

View File

@ -0,0 +1,52 @@
// 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.Collections.Generic;
using Microsoft.Framework.ConfigurationModel;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel;
using Xunit;
namespace Microsoft.AspNet.Mvc.TagHelpers.Test
{
public class TagHelperOptionsCollectionExtensionsTest
{
public static TheoryData ConfigureForm_GetsOptionsFromConfigurationCorrectly_Data
{
get
{
return new TheoryData<string, bool?>
{
{ "true", true },
{ "false", false },
{ "True", true },
{ "False", false },
{ "TRue", true },
{ "FAlse", false },
{ null, null }
};
}
}
[Theory]
[MemberData(nameof(ConfigureForm_GetsOptionsFromConfigurationCorrectly_Data))]
public void ConfigureForm_GetsOptionsFromConfigurationCorrectly(string configValue, bool? expectedValue)
{
// Arrange
var configValues = new Dictionary<string, string>
{
{ $"{nameof(FormTagHelperOptions.GenerateAntiForgeryToken)}", configValue }
};
var config = new Configuration(new MemoryConfigurationSource(configValues));
var services = new ServiceCollection().AddOptions();
services.ConfigureTagHelpers().ConfigureForm(config);
var serviceProvider = services.BuildServiceProvider();
// Act
var options = serviceProvider.GetService<IOptions<FormTagHelperOptions>>().Options;
// Assert
Assert.Equal(expectedValue, options.GenerateAntiForgeryToken);
}
}
}

View File

@ -166,5 +166,10 @@ namespace MvcTagHelpersWebSite.Controllers
{
return View();
}
public IActionResult Form()
{
return View();
}
}
}

View File

@ -0,0 +1,17 @@
@using Microsoft.Framework.DependencyInjection
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
<html>
<head>
<meta charset="utf-8" />
<title>Form</title>
</head>
<body>
<h2>Form Tag Helper Test</h2>
<form asp-controller="MvcTagHelper_Home" asp-action="Form"></form>
<form asp-controller="MvcTagHelper_Home" asp-action="Form" asp-anti-forgery="true"></form>
<form asp-controller="MvcTagHelper_Home" asp-action="Form" asp-anti-forgery="false"></form>
</body>
</html>