| File: FormTagHelper.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.TagHelpers\src\Microsoft.AspNetCore.Mvc.TagHelpers.csproj (Microsoft.AspNetCore.Mvc.TagHelpers) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.TagHelpers; /// <summary> /// <see cref="ITagHelper"/> implementation targeting <form> elements. /// </summary> [HtmlTargetElement("form")] public class FormTagHelper : TagHelper { private const string ActionAttributeName = "asp-action"; private const string AntiforgeryAttributeName = "asp-antiforgery"; private const string AreaAttributeName = "asp-area"; private const string PageAttributeName = "asp-page"; private const string PageHandlerAttributeName = "asp-page-handler"; private const string FragmentAttributeName = "asp-fragment"; private const string ControllerAttributeName = "asp-controller"; private const string RouteAttributeName = "asp-route"; private const string RouteValuesDictionaryName = "asp-all-route-data"; private const string RouteValuesPrefix = "asp-route-"; private const string HtmlActionAttributeName = "action"; private IDictionary<string, string> _routeValues; /// <summary> /// Creates a new <see cref="FormTagHelper"/>. /// </summary> /// <param name="generator">The <see cref="IHtmlGenerator"/>.</param> public FormTagHelper(IHtmlGenerator generator) { Generator = generator; } // This TagHelper's order must be lower than the RenderAtEndOfFormTagHelper. I.e it must be executed before // RenderAtEndOfFormTagHelper does. /// <inheritdoc /> public override int Order => -1000; /// <summary> /// Gets the <see cref="Rendering.ViewContext"/> of the executing view. /// </summary> [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; } /// <summary> /// Gets the <see cref="IHtmlGenerator"/> used to generate the <see cref="FormTagHelper"/>'s output. /// </summary> protected IHtmlGenerator Generator { get; } /// <summary> /// The name of the action method. /// </summary> [HtmlAttributeName(ActionAttributeName)] public string Action { get; set; } /// <summary> /// The name of the controller. /// </summary> [HtmlAttributeName(ControllerAttributeName)] public string Controller { get; set; } /// <summary> /// The name of the area. /// </summary> [HtmlAttributeName(AreaAttributeName)] public string Area { get; set; } /// <summary> /// The name of the page. /// </summary> [HtmlAttributeName(PageAttributeName)] public string Page { get; set; } /// <summary> /// The name of the page handler. /// </summary> [HtmlAttributeName(PageHandlerAttributeName)] public string PageHandler { get; set; } /// <summary> /// Whether the antiforgery token should be generated. /// </summary> /// <value>Defaults to <c>false</c> if user provides an <c>action</c> attribute /// or if the <c>method</c> is <see cref="FormMethod.Get"/>; <c>true</c> otherwise.</value> [HtmlAttributeName(AntiforgeryAttributeName)] public bool? Antiforgery { get; set; } /// <summary> /// Gets or sets the URL fragment. /// </summary> [HtmlAttributeName(FragmentAttributeName)] public string Fragment { get; set; } /// <summary> /// Name of the route. /// </summary> /// <remarks> /// Must be <c>null</c> if <see cref="Action"/> or <see cref="Controller"/> is non-<c>null</c>. /// </remarks> [HtmlAttributeName(RouteAttributeName)] public string Route { get; set; } /// <summary> /// The HTTP method to use. /// </summary> /// <remarks>Passed through to the generated HTML in all cases.</remarks> [EditorBrowsable(EditorBrowsableState.Never)] public string Method { get; set; } /// <summary> /// Additional parameters for the route. /// </summary> [HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)] public IDictionary<string, string> RouteValues { get { if (_routeValues == null) { _routeValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } return _routeValues; } set { _routeValues = value; } } /// <inheritdoc /> /// <remarks> /// Does nothing if user provides an <c>action</c> attribute and <see cref="Antiforgery"/> is <c>null</c> or /// <c>false</c>. /// </remarks> /// <exception cref="InvalidOperationException"> /// Thrown if <c>action</c> attribute is provided and <see cref="Action"/>, <see cref="Controller"/> or <see cref="Fragment"/> are /// non-<c>null</c> or if the user provided <c>asp-route-*</c> attributes. /// </exception> public override void Process(TagHelperContext context, TagHelperOutput output) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(output); if (Method != null) { output.CopyHtmlAttribute(nameof(Method), context); } var antiforgeryDefault = true; var routeableParametersProvided = Action != null || Controller != null || Area != null || Page != null || PageHandler != null || Fragment != null || Route != null || (_routeValues != null && _routeValues.Count > 0); // If "action" is already set, it means the user is attempting to use a normal <form>. if (output.Attributes.TryGetAttribute(HtmlActionAttributeName, out var actionAttribute)) { if (routeableParametersProvided) { // User also specified bound attributes we cannot use. throw new InvalidOperationException( Resources.FormatFormTagHelper_CannotOverrideAction( HtmlActionAttributeName, "<form>", RouteValuesPrefix, ActionAttributeName, ControllerAttributeName, FragmentAttributeName, AreaAttributeName, RouteAttributeName, PageAttributeName, PageHandlerAttributeName)); } 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 or complex IHtmlContent action attribute. // e.g. <form action="" method="..."> or <form action="@CustomUrlIHtmlContent" method="..."> if (string.Equals(Method ?? "get", "get", StringComparison.OrdinalIgnoreCase)) { antiforgeryDefault = false; } else { // Antiforgery default is already set to true } } else { // User is likely using the <form> element to submit to another site. Do not send an antiforgery token to unknown sites. antiforgeryDefault = false; } } else { var routeLink = Route != null; var actionLink = Controller != null || Action != null; var pageLink = Page != null || PageHandler != null; if ((routeLink && actionLink) || (routeLink && pageLink) || (actionLink && pageLink)) { var message = string.Join( Environment.NewLine, Resources.FormatCannotDetermineAttributeFor(HtmlActionAttributeName, "<form>"), RouteAttributeName, ControllerAttributeName + ", " + ActionAttributeName, PageAttributeName); throw new InvalidOperationException(message); } RouteValueDictionary routeValues = null; if (_routeValues != null && _routeValues.Count > 0) { routeValues = new RouteValueDictionary(_routeValues); } if (Area != null) { if (routeValues == null) { routeValues = new RouteValueDictionary(); } // Unconditionally replace any value from asp-route-area. routeValues["area"] = Area; } TagBuilder tagBuilder = null; if (!routeableParametersProvided && _routeValues == null && // Antiforgery will sometime be set globally via TagHelper Initializers, verify it was provided in the cshtml. !context.AllAttributes.ContainsName(AntiforgeryAttributeName)) { // A <form> tag that doesn't utilize asp-* attributes. Let it flow to the output. Method = Method ?? "get"; } else if (pageLink) { tagBuilder = Generator.GeneratePageForm( ViewContext, Page, PageHandler, routeValues, Fragment, method: null, htmlAttributes: null); } else if (routeLink) { tagBuilder = Generator.GenerateRouteForm( ViewContext, Route, routeValues, Fragment, method: null, htmlAttributes: null); } else { tagBuilder = Generator.GenerateForm( ViewContext, Action, Controller, Fragment, routeValues, method: null, htmlAttributes: null); } if (tagBuilder != null) { output.MergeAttributes(tagBuilder); if (tagBuilder.HasInnerHtml) { output.PostContent.AppendHtml(tagBuilder.InnerHtml); } } if (string.Equals(Method, "get", StringComparison.OrdinalIgnoreCase)) { antiforgeryDefault = false; } } if (Antiforgery ?? antiforgeryDefault) { var antiforgeryTag = Generator.GenerateAntiforgery(ViewContext); if (antiforgeryTag != null) { output.PostContent.AppendHtml(antiforgeryTag); } } } }