You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Framework/Connected.Validation/ValidationContext.cs

249 lines
6.7 KiB

using System.Collections;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Web;
using Connected.Configuration;
using Connected.Interop;
using Connected.Middleware;
using Connected.Security.Authorization;
using Connected.ServiceModel;
using Connected.Validation.Annotations;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
namespace Connected.Validation;
internal class ValidationContext : IValidationContext
{
public ValidationContext(IContext context, IAuthorizationContext authorization,
IConfigurationService configuration, IHttpContextAccessor http, IAntiforgery antiforgery,
IMiddlewareService middleware)
{
Context = context;
Authorization = authorization;
Configuration = configuration;
Http = http;
Antiforgery = antiforgery;
Middleware = middleware;
}
private IContext? Context { get; }
private IAuthorizationContext? Authorization { get; }
private IConfigurationService? Configuration { get; }
private IHttpContextAccessor? Http { get; }
private IAntiforgery? Antiforgery { get; }
private IMiddlewareService? Middleware { get; }
public void Validate<TArgs>(ICallerContext context, TArgs value)
where TArgs : IDto
{
ValidateAntiforgery(value);
var results = new List<ValidationResult>();
var refs = new List<object>();
Validate(results, value, refs);
if (results.Any())
throw new ValidationException(results[0].ErrorMessage);
if (Middleware is null)
return;
if (Middleware.Query<IValidator<TArgs>>(context) is not ImmutableList<IValidator<TArgs>> middleware)
return;
foreach (var m in middleware)
m.Validate(value);
}
private async void ValidateAntiforgery(object? value)
{
if (Antiforgery is null || value is null)
return;
if (value.GetType().GetCustomAttribute<ValidateAntiforgeryAttribute>() is ValidateAntiforgeryAttribute attribute && !attribute.ValidateRequest)
return;
if (Configuration is null || Configuration.Type != ProcessType.BackEnd)
return;
if (Http?.HttpContext?.Request is null || !Http.HttpContext.Request.IsAjaxRequest())
return;
/*
* No need to validate antiforgery more than once.
*/
if (Authorization is not null && Authorization.State == AuthorizationContextState.Granted)
return;
if (!await Antiforgery.IsRequestValidAsync(Http.HttpContext))
return;
throw new ValidationException(SR.ValAntiForgery);
}
private void Validate(List<ValidationResult> results, object? value, List<object> references)
{
if (value is null)
return;
if (value.GetType().IsTypePrimitive())
return;
if (value is null || references.Contains(value))
return;
references.Add(value);
var properties = value.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (!properties.Any())
return;
var publicProps = new List<PropertyInfo>();
var nonPublicProps = new List<PropertyInfo>();
foreach (var property in properties)
{
if (property.GetMethod is null)
continue;
if (property.GetCustomAttribute<SkipValidationAttribute>() is not null)
continue;
if (property.GetMethod.IsPublic)
publicProps.Add(property);
else
nonPublicProps.Add(property);
}
/*
* First, iterate only through the public properties
* At this point we won't validate complex objects, only the attributes directly on the
* passed instance
*/
foreach (var property in publicProps)
ValidateProperty(results, value, property);
/*
* If root validation failed we won't go deep because this would probably cause
* duplicate and/or confusing validation messages
*/
if (results.Any())
return;
/*
* Second step is to validate complex public members and collections.
*/
foreach (var property in publicProps)
{
if (property.PropertyType.IsEnumerable())
{
if (GetValue(value, property) is not IEnumerable ien)
continue;
var en = ien.GetEnumerator();
while (en.MoveNext())
{
if (en.Current is null)
continue;
Validate(results, en.Current, references);
}
}
else
{
if (GetValue(value, property) is not object val)
continue;
Validate(results, val, references);
}
}
/*
* If any complex validation failed we won't validate private members because
* it is possible that initialization would fail for the reason of validation being failed.
*/
if (results.Any())
return;
/*
* Now that validation of the public properties succeed we can go validate nonpublic members
*/
foreach (var property in nonPublicProps)
ValidateProperty(results, value, property);
}
private void ValidateProperty(List<ValidationResult> results, object? value, PropertyInfo property)
{
var attributes = property.GetCustomAttributes(false);
if (!ValidateRequestValue(results, value, property))
return;
if (property.PropertyType.IsEnum && !Enum.TryParse(property.PropertyType, TypeConversion.Convert<string>(property.GetValue(value)), out _))
results.Add(new ValidationResult($"{SR.ValEnumValueNotDefined} ({property.PropertyType.ShortName()}, {property.GetValue(value)})", new string[] { property.Name }));
foreach (var attribute in attributes)
{
if (attribute is ValidationAttribute val)
{
var serviceProvider = new ValidationServiceProvider(Context);
var displayName = property.Name;
var ctx = new System.ComponentModel.DataAnnotations.ValidationContext(value, serviceProvider, new Dictionary<object, object?>())
{
DisplayName = displayName.ToLower(),
MemberName = property.Name,
};
val.Validate(GetValue(value, property), ctx);
}
}
}
private static bool ValidateRequestValue(List<ValidationResult> results, PropertyInfo property, object? value)
{
if (value is null)
return true;
var att = property.FindAttribute<ValidateRequestAttribute>();
if (att is not null && !att.Validate)
return true;
if (HttpUtility.HtmlDecode(value.ToString()) is not string decoded)
return true;
if (decoded.Replace(" ", string.Empty).Contains("<script>"))
{
results.Add(new ValidationResult(SR.ValScriptTagNotAllowed, new string[] { property.Name }));
return false;
}
return true;
}
private static bool ValidateRequestValue(List<ValidationResult> results, object value, PropertyInfo property)
{
if (property.PropertyType != typeof(string))
return true;
if (!property.CanWrite)
return true;
return ValidateRequestValue(results, property, GetValue(value, property));
}
private static object? GetValue(object value, PropertyInfo property)
{
try
{
return property.GetValue(value);
}
catch (TargetInvocationException ex)
{
if (ex.InnerException is ValidationException)
throw ex.InnerException;
throw ex;
}
}
}