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.
249 lines
6.7 KiB
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;
|
|
}
|
|
}
|
|
}
|