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(ICallerContext context, TArgs value) where TArgs : IDto { ValidateAntiforgery(value); var results = new List(); var refs = new List(); Validate(results, value, refs); if (results.Any()) throw new ValidationException(results[0].ErrorMessage); if (Middleware is null) return; if (Middleware.Query>(context) is not ImmutableList> 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() 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 results, object? value, List 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(); var nonPublicProps = new List(); foreach (var property in properties) { if (property.GetMethod is null) continue; if (property.GetCustomAttribute() 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 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(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()) { DisplayName = displayName.ToLower(), MemberName = property.Name, }; val.Validate(GetValue(value, property), ctx); } } } private static bool ValidateRequestValue(List results, PropertyInfo property, object? value) { if (value is null) return true; var att = property.FindAttribute(); 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("