// Copyright (c) .NET Foundation. 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
public class DefaultHtmlGenerator : IHtmlGenerator
{
private const string HiddenListItem = @"
";
private static readonly MethodInfo ConvertEnumFromStringMethod =
typeof(DefaultHtmlGenerator).GetTypeInfo().GetDeclaredMethod(nameof(ConvertEnumFromString));
// See: (http://www.w3.org/TR/html5/forms.html#the-input-element)
private static readonly string[] _placeholderInputTypes =
new[] { "text", "search", "url", "tel", "email", "password", "number" };
private readonly IAntiforgery _antiforgery;
private readonly IClientModelValidatorProvider _clientModelValidatorProvider;
private readonly IModelMetadataProvider _metadataProvider;
private readonly IUrlHelperFactory _urlHelperFactory;
private readonly HtmlEncoder _htmlEncoder;
private readonly ClientValidatorCache _clientValidatorCache;
///
/// Initializes a new instance of the class.
///
/// The instance which is used to generate antiforgery
/// tokens.
/// The accessor for .
/// The .
/// The .
/// The .
/// The that provides
/// a list of s.
public DefaultHtmlGenerator(
IAntiforgery antiforgery,
IOptions optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache)
{
if (antiforgery == null)
{
throw new ArgumentNullException(nameof(antiforgery));
}
if (optionsAccessor == null)
{
throw new ArgumentNullException(nameof(optionsAccessor));
}
if (metadataProvider == null)
{
throw new ArgumentNullException(nameof(metadataProvider));
}
if (urlHelperFactory == null)
{
throw new ArgumentNullException(nameof(urlHelperFactory));
}
if (htmlEncoder == null)
{
throw new ArgumentNullException(nameof(htmlEncoder));
}
if (clientValidatorCache == null)
{
throw new ArgumentNullException(nameof(clientValidatorCache));
}
_antiforgery = antiforgery;
var clientValidatorProviders = optionsAccessor.Value.ClientModelValidatorProviders;
_clientModelValidatorProvider = new CompositeClientModelValidatorProvider(clientValidatorProviders);
_metadataProvider = metadataProvider;
_urlHelperFactory = urlHelperFactory;
_htmlEncoder = htmlEncoder;
_clientValidatorCache = clientValidatorCache;
// Underscores are fine characters in id's.
IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement;
}
///
public string IdAttributeDotReplacement { get; }
///
public string Encode(string value)
{
return !string.IsNullOrEmpty(value) ? _htmlEncoder.Encode(value) : string.Empty;
}
///
public string Encode(object value)
{
return (value != null) ? _htmlEncoder.Encode(value.ToString()) : string.Empty;
}
///
public string FormatValue(object value, string format)
{
return ViewDataDictionary.FormatValue(value, format);
}
///
public virtual TagBuilder GenerateActionLink(
ViewContext viewContext,
string linkText,
string actionName,
string controllerName,
string protocol,
string hostname,
string fragment,
object routeValues,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (linkText == null)
{
throw new ArgumentNullException(nameof(linkText));
}
var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext);
var url = urlHelper.Action(actionName, controllerName, routeValues, protocol, hostname, fragment);
return GenerateLink(linkText, url, htmlAttributes);
}
///
public virtual IHtmlContent GenerateAntiforgery(ViewContext viewContext)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
// If we're inside a BeginForm/BeginRouteForm, the antiforgery token might have already been
// created and appended to the 'end form' content OR the form tag helper might have already generated
// an antiforgery token.
if (viewContext.FormContext.HasAntiforgeryToken)
{
return HtmlString.Empty;
}
viewContext.FormContext.HasAntiforgeryToken = true;
return _antiforgery.GetHtml(viewContext.HttpContext);
}
///
public virtual TagBuilder GenerateCheckBox(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
bool? isChecked,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (modelExplorer != null)
{
// CheckBoxFor() case. That API does not support passing isChecked directly.
Debug.Assert(!isChecked.HasValue);
if (modelExplorer.Model != null)
{
bool modelChecked;
if (bool.TryParse(modelExplorer.Model.ToString(), out modelChecked))
{
isChecked = modelChecked;
}
}
}
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (isChecked.HasValue && htmlAttributeDictionary != null)
{
// Explicit isChecked value must override "checked" in dictionary.
htmlAttributeDictionary.Remove("checked");
}
// Use ViewData only in CheckBox case (metadata null) and when the user didn't pass an isChecked value.
return GenerateInput(
viewContext,
InputType.CheckBox,
modelExplorer,
expression,
value: "true",
useViewData: (modelExplorer == null && !isChecked.HasValue),
isChecked: isChecked ?? false,
setId: true,
isExplicitValue: false,
format: null,
htmlAttributes: htmlAttributeDictionary);
}
///
public virtual TagBuilder GenerateHiddenForCheckbox(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
var tagBuilder = new TagBuilder("input");
tagBuilder.MergeAttribute("type", GetInputTypeString(InputType.Hidden));
tagBuilder.MergeAttribute("value", "false");
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
var fullName = GetFullHtmlFieldName(viewContext, expression);
tagBuilder.MergeAttribute("name", fullName);
return tagBuilder;
}
///
public virtual TagBuilder GenerateForm(
ViewContext viewContext,
string actionName,
string controllerName,
object routeValues,
string method,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
var defaultMethod = false;
if (string.IsNullOrEmpty(method))
{
defaultMethod = true;
}
else if (string.Equals(method, "post", StringComparison.OrdinalIgnoreCase))
{
defaultMethod = true;
}
string action;
if (actionName == null && controllerName == null && routeValues == null && defaultMethod)
{
// Submit to the original URL in the special case that user called the BeginForm() overload without
// parameters (except for the htmlAttributes parameter). Also reachable in the even-more-unusual case
// that user called another BeginForm() overload with default argument values.
var request = viewContext.HttpContext.Request;
action = request.PathBase + request.Path + request.QueryString;
}
else
{
var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext);
action = urlHelper.Action(action: actionName, controller: controllerName, values: routeValues);
}
return GenerateFormCore(viewContext, action, method, htmlAttributes);
}
///
public TagBuilder GenerateRouteForm(
ViewContext viewContext,
string routeName,
object routeValues,
string method,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext);
var action = urlHelper.RouteUrl(routeName, routeValues);
return GenerateFormCore(viewContext, action, method, htmlAttributes);
}
///
public virtual TagBuilder GenerateHidden(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
object value,
bool useViewData,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
// Special-case opaque values and arbitrary binary data.
var byteArrayValue = value as byte[];
if (byteArrayValue != null)
{
value = Convert.ToBase64String(byteArrayValue);
}
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
return GenerateInput(
viewContext,
InputType.Hidden,
modelExplorer,
expression,
value,
useViewData,
isChecked: false,
setId: true,
isExplicitValue: true,
format: null,
htmlAttributes: htmlAttributeDictionary);
}
///
public virtual TagBuilder GenerateLabel(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
string labelText,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (modelExplorer == null)
{
throw new ArgumentNullException(nameof(modelExplorer));
}
var resolvedLabelText = labelText ??
modelExplorer.Metadata.DisplayName ??
modelExplorer.Metadata.PropertyName;
if (resolvedLabelText == null)
{
resolvedLabelText =
string.IsNullOrEmpty(expression) ? string.Empty : expression.Split('.').Last();
}
if (string.IsNullOrEmpty(resolvedLabelText))
{
return null;
}
var tagBuilder = new TagBuilder("label");
var idString =
TagBuilder.CreateSanitizedId(GetFullHtmlFieldName(viewContext, expression), IdAttributeDotReplacement);
tagBuilder.Attributes.Add("for", idString);
tagBuilder.InnerHtml.SetContent(resolvedLabelText);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), replaceExisting: true);
return tagBuilder;
}
///
public virtual TagBuilder GeneratePassword(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
object value,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
return GenerateInput(
viewContext,
InputType.Password,
modelExplorer,
expression,
value,
useViewData: false,
isChecked: false,
setId: true,
isExplicitValue: true,
format: null,
htmlAttributes: htmlAttributeDictionary);
}
///
public virtual TagBuilder GenerateRadioButton(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
object value,
bool? isChecked,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (modelExplorer == null)
{
// RadioButton() case. Do not override checked attribute if isChecked is implicit.
if (!isChecked.HasValue &&
(htmlAttributeDictionary == null || !htmlAttributeDictionary.ContainsKey("checked")))
{
// Note value may be null if isChecked is non-null.
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
// isChecked not provided nor found in the given attributes; fall back to view data.
var valueString = Convert.ToString(value, CultureInfo.CurrentCulture);
isChecked = string.Equals(
EvalString(viewContext, expression),
valueString,
StringComparison.OrdinalIgnoreCase);
}
}
else
{
// RadioButtonFor() case. That API does not support passing isChecked directly.
Debug.Assert(!isChecked.HasValue);
// Need a value to determine isChecked.
Debug.Assert(value != null);
var model = modelExplorer.Model;
var valueString = Convert.ToString(value, CultureInfo.CurrentCulture);
isChecked = model != null &&
string.Equals(model.ToString(), valueString, StringComparison.OrdinalIgnoreCase);
}
if (isChecked.HasValue && htmlAttributeDictionary != null)
{
// Explicit isChecked value must override "checked" in dictionary.
htmlAttributeDictionary.Remove("checked");
}
return GenerateInput(
viewContext,
InputType.Radio,
modelExplorer,
expression,
value,
useViewData: false,
isChecked: isChecked ?? false,
setId: true,
isExplicitValue: true,
format: null,
htmlAttributes: htmlAttributeDictionary);
}
///
public virtual TagBuilder GenerateRouteLink(
ViewContext viewContext,
string linkText,
string routeName,
string protocol,
string hostName,
string fragment,
object routeValues,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (linkText == null)
{
throw new ArgumentNullException(nameof(linkText));
}
var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext);
var url = urlHelper.RouteUrl(routeName, routeValues, protocol, hostName, fragment);
return GenerateLink(linkText, url, htmlAttributes);
}
///
public TagBuilder GenerateSelect(
ViewContext viewContext,
ModelExplorer modelExplorer,
string optionLabel,
string expression,
IEnumerable selectList,
bool allowMultiple,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
var currentValues = GetCurrentValues(viewContext, modelExplorer, expression, allowMultiple);
return GenerateSelect(
viewContext,
modelExplorer,
optionLabel,
expression,
selectList,
currentValues,
allowMultiple,
htmlAttributes);
}
///
public virtual TagBuilder GenerateSelect(
ViewContext viewContext,
ModelExplorer modelExplorer,
string optionLabel,
string expression,
IEnumerable selectList,
ICollection currentValues,
bool allowMultiple,
object htmlAttributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
var fullName = GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
typeof(IHtmlHelper).FullName,
nameof(IHtmlHelper.Editor),
typeof(IHtmlHelper<>).FullName,
nameof(IHtmlHelper