Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Change FormTagHelper to apply to all form tags.
Browse files Browse the repository at this point in the history
- Added functional test to validate that non-attributed form tags have an antiforgery input generated. Re-generated baseline to reflect changes.
- Added a unit test to validate that parameterless `FormTagHelper`s behave as expected.

#6006
  • Loading branch information
NTaylorMullen committed Apr 21, 2017
1 parent 908b4c8 commit 8ea8521
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 31 deletions.
60 changes: 42 additions & 18 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/FormTagHelper.cs
Expand Up @@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
Expand All @@ -13,14 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;form&gt; elements.
/// </summary>
[HtmlTargetElement("form", Attributes = ActionAttributeName)]
[HtmlTargetElement("form", Attributes = AntiforgeryAttributeName)]
[HtmlTargetElement("form", Attributes = AreaAttributeName)]
[HtmlTargetElement("form", Attributes = FragmentAttributeName)]
[HtmlTargetElement("form", Attributes = ControllerAttributeName)]
[HtmlTargetElement("form", Attributes = RouteAttributeName)]
[HtmlTargetElement("form", Attributes = RouteValuesDictionaryName)]
[HtmlTargetElement("form", Attributes = RouteValuesPrefix + "*")]
[HtmlTargetElement("form")]
public class FormTagHelper : TagHelper
{
private const string ActionAttributeName = "asp-action";
Expand Down Expand Up @@ -143,15 +138,20 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
{
throw new ArgumentNullException(nameof(output));
}

if (Method != null)
{
output.CopyHtmlAttribute(nameof(Method), context);
}
else
{
Method = "get";
}

var antiforgeryDefault = true;

// If "action" is already set, it means the user is attempting to use a normal <form>.
if (output.Attributes.ContainsName(HtmlActionAttributeName))
if (output.Attributes.TryGetAttribute(HtmlActionAttributeName, out var actionAttribute))
{
if (Action != null ||
Controller != null ||
Expand All @@ -173,9 +173,29 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
RouteValuesPrefix));
}

// User is using the FormTagHelper like a normal <form> tag. Antiforgery default should be false to
// not force the antiforgery token on the user.
antiforgeryDefault = false;
string attributeValue = null;
switch (actionAttribute.Value)
{
case HtmlString htmlString:
attributeValue = htmlString.ToString();
break;
case string stringValue:
attributeValue = stringValue;
break;
}

if (string.IsNullOrEmpty(attributeValue))
{
// User is using the FormTagHelper like a normal <form> tag that has an empty action attribute.
// i.e. <form action="" method="post">
antiforgeryDefault = true;
}
else
{
// User is using the FormTagHelper like a normal <form> tag. Antiforgery default should be false to
// not force the antiforgery token on the user.
antiforgeryDefault = false;
}
}
else
{
Expand All @@ -201,8 +221,12 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
routeValues["area"] = Area;
}

TagBuilder tagBuilder;
if (Route == null)
TagBuilder tagBuilder = null;
if (Action == null && Controller == null && Route == null && _routeValues == null && Fragment == null && Area == null)
{
// Empty form tag such as <form></form>. Let it flow to the output as-is and only handle anti-forgery.
}
else if (Route == null)
{
tagBuilder = Generator.GenerateForm(
ViewContext,
Expand Down Expand Up @@ -244,11 +268,11 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
output.PostContent.AppendHtml(tagBuilder.InnerHtml);
}
}
}

if (string.Equals(Method, "get", StringComparison.OrdinalIgnoreCase))
{
antiforgeryDefault = false;
}
if (string.Equals(Method, "get", StringComparison.OrdinalIgnoreCase))
{
antiforgeryDefault = false;
}

if (Antiforgery ?? antiforgeryDefault)
Expand Down
Expand Up @@ -19,21 +19,15 @@ public static string RetrieveAntiforgeryToken(string htmlContent, string actionU

foreach (var form in htmlDocument.Descendants("form"))
{
foreach (var attribute in form.Attributes())
foreach (var input in form.Descendants("input"))
{
if (string.Equals(attribute.Name.LocalName, "action", StringComparison.OrdinalIgnoreCase))
if (input.Attribute("name") != null &&
input.Attribute("type") != null &&
input.Attribute("type").Value == "hidden" &&
(input.Attribute("name").Value == "__RequestVerificationToken" ||
input.Attribute("name").Value == "HtmlEncode[[__RequestVerificationToken]]"))
{
foreach (var input in form.Descendants("input"))
{
if (input.Attribute("name") != null &&
input.Attribute("type") != null &&
input.Attribute("type").Value == "hidden" &&
(input.Attribute("name").Value == "__RequestVerificationToken" ||
input.Attribute("name").Value == "HtmlEncode[[__RequestVerificationToken]]"))
{
return input.Attributes("value").First().Value;
}
}
return input.Attributes("value").First().Value;
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
Expand Up @@ -6,20 +6,53 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class RazorPagesTest : IClassFixture<MvcTestFixture<RazorPagesWebSite.Startup>>
{
private static readonly Assembly _resourcesAssembly = typeof(RazorPagesTest).GetTypeInfo().Assembly;

public RazorPagesTest(MvcTestFixture<RazorPagesWebSite.Startup> fixture)
{
Client = fixture.Client;
}

public HttpClient Client { get; }

[Fact]
public async Task Page_SimpleForms_RenderAntiforgery()
{
// Arrange
var expectedMediaType = MediaTypeHeaderValue.Parse("text/html; charset=utf-8");
var outputFile = "compiler/resources/RazorPagesWebSite.SimpleForms.html";
var expectedContent = await ResourceFile.ReadResourceAsync(_resourcesAssembly, outputFile, sourceFile: false);

// Act
var response = await Client.GetAsync("http://localhost/SimpleForms");
var responseContent = await response.Content.ReadAsStringAsync();

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedMediaType, response.Content.Headers.ContentType);

responseContent = responseContent.Trim();

var forgeryToken = AntiforgeryTestHelper.RetrieveAntiforgeryToken(responseContent, "SimpleForms");
#if GENERATE_BASELINES
// Reverse usual substitution and insert a format item into the new file content.
responseContent = responseContent.Replace(forgeryToken, "{0}");
ResourceFile.UpdateFile(_resourcesAssembly, outputFile, expectedContent, responseContent);
#else
expectedContent = string.Format(expectedContent, forgeryToken);
Assert.Equal(expectedContent.Trim(), responseContent, ignoreLineEndingDifferences: true);
#endif
}

[Fact]
public async Task Page_Handler_FormActionFromQueryString()
{
Expand Down
@@ -0,0 +1,6 @@
<form></form>
<form method="get"></form>
<form method="post"><input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
<form action="" method="post"><input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
<form action="/Foo/Bar/Baz.html" method="get"></form>
<form action="/Foo/Bar/Baz.html" method="post"></form>
50 changes: 50 additions & 0 deletions test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/FormTagHelperTest.cs
Expand Up @@ -26,6 +26,56 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class FormTagHelperTest
{
[Fact]
public async Task ProcessAsync_NoParametersOnlyGeneratesAntiforgery()
{
// Arrange
var expectedTagName = "form";
var metadataProvider = new TestModelMetadataProvider();
var tagHelperContext = new TagHelperContext(
tagName: "form",
allAttributes: new TagHelperAttributeList(),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
expectedTagName,
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index");

var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
var viewContext = TestableHtmlGenerator.GetViewContext(
model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var expectedPostContent = HtmlContentUtilities.HtmlContentToString(
htmlGenerator.GenerateAntiforgery(viewContext),
HtmlEncoder.Default);
var formTagHelper = new FormTagHelper(htmlGenerator)
{
ViewContext = viewContext,
};

// Act
await formTagHelper.ProcessAsync(tagHelperContext, output);

// Assert
Assert.Empty(output.Attributes);
Assert.Empty(output.PreElement.GetContent());
Assert.Empty(output.PreContent.GetContent());
Assert.Empty(output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Assert.Empty(output.PostElement.GetContent());
Assert.Equal(expectedTagName, output.TagName);
}

[Fact]
public async Task ProcessAsync_GeneratesExpectedOutput()
{
Expand Down
9 changes: 9 additions & 0 deletions test/WebSites/RazorPagesWebSite/SimpleForms.cshtml
@@ -0,0 +1,9 @@
@page
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"

<form></form>
<form method="get"></form>
<form method="post"></form>
<form action="" method="post"></form>
<form action="/Foo/Bar/Baz.html" method="get"></form>
<form action="/Foo/Bar/Baz.html" method="post"></form>

0 comments on commit 8ea8521

Please sign in to comment.