using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Finch.Security;
///
/// Provides cached values for "name" and "id" HTML attributes.
/// Copied from
/// https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ViewFeatures/src/NameAndIdProvider.cs
/// as it is marked as internal and can't be used otherwise
///
internal static class NameAndIdProviderCopy
{
private static readonly object PreviousNameAndIdKey = typeof(PreviousNameAndId);
///
/// Returns a valid HTML 4.01 "id" attribute value for an element with the given .
///
/// A instance for the current scope.
///
/// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
///
///
/// The (normally a single ) to substitute for invalid characters in
/// .
///
///
/// Valid HTML 4.01 "id" attribute value for an element with the given .
///
///
/// Similar to but caches value for repeated invocations.
///
public static string CreateSanitizedId(ViewContext viewContext, string fullName, string invalidCharReplacement)
{
ArgumentNullException.ThrowIfNull(viewContext);
ArgumentNullException.ThrowIfNull(invalidCharReplacement);
if (string.IsNullOrEmpty(fullName))
{
return string.Empty;
}
// Check cache to avoid whatever TagBuilder.CreateSanitizedId() may do.
var items = viewContext.HttpContext.Items;
object previousNameAndIdObject;
PreviousNameAndId previousNameAndId = null;
if (items.TryGetValue(PreviousNameAndIdKey, out previousNameAndIdObject) &&
(previousNameAndId = (PreviousNameAndId)previousNameAndIdObject) != null &&
string.Equals(previousNameAndId.FullName, fullName, StringComparison.Ordinal))
{
return previousNameAndId.SanitizedId;
}
var sanitizedId = TagBuilder.CreateSanitizedId(fullName, invalidCharReplacement);
if (previousNameAndId == null)
{
// Do not create a PreviousNameAndId when TagBuilder.CreateSanitizedId() only examined fullName.
if (string.Equals(fullName, sanitizedId, StringComparison.Ordinal))
{
return sanitizedId;
}
previousNameAndId = new PreviousNameAndId();
items[PreviousNameAndIdKey] = previousNameAndId;
}
previousNameAndId.FullName = fullName;
previousNameAndId.SanitizedId = sanitizedId;
return previousNameAndId.SanitizedId;
}
///
/// Adds a valid HTML 4.01 "id" attribute for an element with the given . Does
/// nothing if already contains an "id" attribute or the
/// is null or empty.
///
/// A instance for the current scope.
/// A instance that will contain the "id" attribute.
///
/// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
///
///
/// The (normally a single ) to substitute for invalid characters in
/// .
///
///
/// Similar to but caches value for repeated invocations.
///
///
public static void GenerateId(
ViewContext viewContext,
TagBuilder tagBuilder,
string fullName,
string invalidCharReplacement)
{
ArgumentNullException.ThrowIfNull(viewContext);
ArgumentNullException.ThrowIfNull(tagBuilder);
ArgumentNullException.ThrowIfNull(invalidCharReplacement);
if (string.IsNullOrEmpty(fullName))
{
return;
}
if (!tagBuilder.Attributes.ContainsKey("id"))
{
var sanitizedId = CreateSanitizedId(viewContext, fullName, invalidCharReplacement);
// Duplicate check for null or empty to cover the corner case where fullName contains only invalid
// characters and invalidCharReplacement is empty.
if (!string.IsNullOrEmpty(sanitizedId))
{
tagBuilder.Attributes["id"] = sanitizedId;
}
}
}
///
/// Returns the full HTML element name for the specified .
///
/// A instance for the current scope.
/// Expression name, relative to the current model.
/// Fully-qualified expression name for .
///
/// Similar to but caches value for repeated invocations.
///
public static string GetFullHtmlFieldName(ViewContext viewContext, string expression)
{
var htmlFieldPrefix = viewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
if (string.IsNullOrEmpty(expression))
{
return htmlFieldPrefix;
}
if (string.IsNullOrEmpty(htmlFieldPrefix))
{
return expression;
}
// Need to concatenate. See if we've already done that.
var items = viewContext.HttpContext.Items;
object previousNameAndIdObject;
PreviousNameAndId previousNameAndId = null;
if (items.TryGetValue(PreviousNameAndIdKey, out previousNameAndIdObject) &&
(previousNameAndId = (PreviousNameAndId)previousNameAndIdObject) != null &&
string.Equals(previousNameAndId.HtmlFieldPrefix, htmlFieldPrefix, StringComparison.Ordinal) &&
string.Equals(previousNameAndId.Expression, expression, StringComparison.Ordinal))
{
return previousNameAndId.OutputFullName;
}
if (previousNameAndId == null)
{
previousNameAndId = new PreviousNameAndId();
items[PreviousNameAndIdKey] = previousNameAndId;
}
previousNameAndId.HtmlFieldPrefix = htmlFieldPrefix;
previousNameAndId.Expression = expression;
if (expression.StartsWith('['))
{
// The expression might represent an indexer access, in which case with a 'dot' would be invalid.
previousNameAndId.OutputFullName = htmlFieldPrefix + expression;
}
else
{
previousNameAndId.OutputFullName = htmlFieldPrefix + "." + expression;
}
return previousNameAndId.OutputFullName;
}
private sealed class PreviousNameAndId
{
// Cached ambient input for NameAndIdProvider.GetFullHtmlFieldName(). TemplateInfo.HtmlFieldPrefix may
// change during the lifetime of a ViewContext.
public string HtmlFieldPrefix { get; set; }
// Cached input for NameAndIdProvider.GetFullHtmlFieldName().
public string Expression { get; set; }
// Cached return value for NameAndIdProvider.GetFullHtmlFieldName().
public string OutputFullName { get; set; }
// Cached input for NameAndIdProvider.CreateSanitizedId(). Since IHtmlHelper.GenerateIdFromName() is
// available to all, there is no guarantee this is equal to OutputFullName when CreateSanitizedId() is
// called.
public string FullName { get; set; }
// Cached return value for NameAndIdProvider.CreateSanitizedId().
public string SanitizedId { get; set; }
}
}