This commit is contained in:
2025-05-05 14:45:58 +03:00
parent 0fb4058186
commit 90dd893f3e
43 changed files with 2501 additions and 63 deletions

View File

@@ -0,0 +1,118 @@
using System.ComponentModel.DataAnnotations;
using anbs_cp.Classes;
using anbs_cp.ForNet.Constants;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace anbs_cp.ForNet.Classes;
/// <summary>
/// Кастомные сообщения валидации формы
/// </summary>
/// <param name="messageList">Кастомный список сообщений формы</param>
public class CustomValidationMessages (KeyValueOrderedList<string>? messageList): IValidationMetadataProvider
{
/// <summary>
/// Список сообщений формы
/// </summary>
// ReSharper disable once MemberCanBePrivate.Global
public readonly KeyValueOrderedList<string> MessageList = messageList ?? ValidationMessagesConstants.English;
/// <summary>
/// Создаёт данные для локализации сообщений валидации
/// </summary>
/// <param name="context">Контекст</param>
public void CreateValidationMetadata (ValidationMetadataProviderContext context)
{
// Если context = null, то прерываем с ошибкой
ArgumentNullException.ThrowIfNull(context);
// Получаю мета-данные
IList<object>? validators = context.ValidationMetadata.ValidatorMetadata;
// Получаю тип модели
Type? theType = context.Key.ModelType;
// Получаю фундаментальный тип
Type? underlyingType = Nullable.GetUnderlyingType(theType);
// Проверяю получение и соответствие
if (theType.IsValueType && underlyingType == null &&
validators.All(m => m.GetType() != typeof(RequiredAttribute)))
// - задаю валидацию
validators.Add(new RequiredAttribute());
// Для каждого объекта валидации
foreach (object? obj in validators)
{
// - если он не атрибут
if (obj is not ValidationAttribute attribute)
// -- то продолжаю
continue;
// RequiredAttribute
FillErrorMessage<RequiredAttribute>(attribute);
// MinLengthAttribute
FillErrorMessage<MinLengthAttribute>(attribute);
// MaxLengthAttribute
FillErrorMessage<MaxLengthAttribute>(attribute);
// EmailAddressAttribute
FillErrorMessage<EmailAddressAttribute>(attribute);
// RangeAttribute
FillErrorMessage<RangeAttribute>(attribute);
// CompareAttribute
FillErrorMessage<CompareAttribute>(attribute);
// RegularExpressionAttribute
FillErrorMessage<RegularExpressionAttribute>(attribute);
// PhoneAttribute
FillErrorMessage<PhoneAttribute>(attribute);
// CreditCardAttribute
FillErrorMessage<CreditCardAttribute>(attribute);
}
}
/// <summary>
/// Устанавливает сообщение об ошибке валидации
/// </summary>
/// <typeparam name="T">Тип атрибута валидации</typeparam>
/// <param name="attribute">Атрибут</param>
private void FillErrorMessage<T> (object attribute) where T : ValidationAttribute
{
// Проверяю на соответствие типа
if (attribute is not T validationAttribute)
// - и прерываю, если не соответствует
return;
// Если нет сообщения об ошибке
if (validationAttribute.ErrorMessage == null && validationAttribute.ErrorMessageResourceName == null)
{
// - получаю имя аттрибута
string attributeTypeName = typeof(T).Name;
// - если список сообщений не содержит ключа с таким именем аттрибута
if (MessageList.Keys.All(s => s != attributeTypeName))
// -- то прерываю
return;
// - получаю предопределённое сообщение об ошибке
string? errorMessage = MessageList.GetValue(attributeTypeName);
// - если предопределённое сообщение пусто
if (errorMessage == null)
// -- то прерываю
return;
// - задаю значение
validationAttribute.ErrorMessage = errorMessage;
}
}
}

View File

@@ -0,0 +1,41 @@
using anbs_cp.Classes;
namespace anbs_cp.ForNet.Constants;
/// <summary>
/// Сообщения валидации
/// </summary>
public static class ValidationMessagesConstants
{
/// <summary>
/// Сообщения на русском языке
/// </summary>
public static readonly KeyValueOrderedList<string> Russian =
[
new("RequiredAttribute", "Поле '{0}' обязательно к заполнению."),
new("MinLengthAttribute", "Минимальная длина поля '{0}' от {1} символа(-ов)."),
new("MaxLengthAttribute", "Максимальная длина поля '{0}' до {1} символа(-ов)."),
new("EmailAddressAttribute", "Адрес email некорректен."),
new("RangeAttribute", "Значение поля '{0}' должно быть между {1} и {2}."),
new("CompareAttribute", "Значение поля '{0}' не совпадаем с требуемым."),
new("RegularExpressionAttribute", "Значение поля '{0}' не удовлетворяет шаблону."),
new ("PhoneAttribute", "Значение поля '{0}' не является номером телефона."),
new ("CreditCardAttribute", "Значение поля '{0}' не является номером кредитной карты.")
];
/// <summary>
/// Сообщения на английском языке
/// </summary>
public static readonly KeyValueOrderedList<string> English =
[
new("RequiredAttribute", "You must fill in '{0}'."),
new("MinLengthAttribute", "Min length of '{0}' is {1}."),
new("MaxLengthAttribute", "Max length of '{0}' is {1}."),
new("EmailAddressAttribute", "Invalid email address."),
new("RangeAttribute", "The value of the field '{0}' must be between {1} and {2}."),
new("CompareAttribute", "Unmatched field '{0}' value."),
new("RegularExpressionAttribute", "Invalid field '{0}' value."),
new ("PhoneAttribute", "The value of the field '{0}' is not valid phone number."),
new ("CreditCardAttribute", "The value of the field '{0}' is not valid credit card number.")
];
}

View File

@@ -0,0 +1,108 @@
using System.Text.RegularExpressions;
using ImageMagick;
using Microsoft.AspNetCore.Http;
using static System.Text.RegularExpressions.Regex;
namespace anbs_cp.ForNet.Extensions;
/// <summary>
/// Расширение интерфейса IFormFile
/// </summary>
public static class FormFileExtension
{
#region Константы
/// <summary>
/// Минимальный размер изображения (в байтах)
/// </summary>
private const int ImageMinimumBytes = 512;
/// <summary>
/// Список поддерживаемых Mime-типов
/// </summary>
private static readonly List<string> AllowedMimeTypes =
["image/jpg", "image/jpeg", "image/pjpeg", "image/gif", "image/x-png", "image/png"];
/// <summary>
/// Список поддерживаемых расширений файлов
/// </summary>
private static readonly List<string> AllowedExtensions = [".jpg", ".png", ".gif", ".jpeg"];
#endregion
/// <summary>
/// Получение содержимого файла
/// </summary>
/// <param name="file">Файл из формы</param>
/// <returns>Содержимое файла</returns>
public static byte[] GetFileContent (this IFormFile file)
{
// Читаем содержимое файла
using BinaryReader binaryReader = new(file.OpenReadStream());
// Создаю контент изображения
byte[] content = binaryReader.ReadBytes((int)file.Length);
// Вывожу результат
return content;
}
/// <summary>
/// Проверка, является ли файл изображением
/// </summary>
/// <param name="postedFile">Файл из формы</param>
/// <returns>Является ли файл изображением</returns>
public static bool IsImage (this IFormFile postedFile)
{
// Проверяю Mime-тип
if (!AllowedMimeTypes.Contains(postedFile.ContentType.ToLower()))
return false;
// Проверяю расширение
if (!AllowedExtensions.Contains(Path.GetExtension(postedFile.FileName).ToLower()))
return false;
// Пытаюсь прочитать файл и проверить первые байты
try
{
if (!postedFile.OpenReadStream().CanRead)
return false;
// Проверяю, не меньше ли размер изображения установленного предела
if (postedFile.Length < ImageMinimumBytes)
return false;
byte[] buffer = new byte[ImageMinimumBytes];
int _ = postedFile.OpenReadStream().Read(buffer, 0, ImageMinimumBytes);
string content = System.Text.Encoding.UTF8.GetString(buffer);
if (IsMatch(content,
@"<script|<html|<head|<title|<body|<pre|<table|<a\s+href|<img|<plaintext|<cross\-domain\-policy",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline))
return false;
}
catch (Exception)
{
// - если .NET выдаст исключение, то можно предположить, что это недопустимое изображение
return false;
}
// Попробую создать экземпляр MagickImageCollection,
try
{
using MagickImageCollection images = new(postedFile.OpenReadStream());
}
catch (Exception)
{
// - если .NET выдаст исключение, то можно предположить, что это недопустимое изображение
return false;
}
finally
{
postedFile.OpenReadStream().Position = 0;
}
// Возвращаю, что это изображение
return true;
}
}

View File

@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>ANBSoftware.ComponentsPackForNet</PackageId>
<Version>2024.3.27</Version>
<Version>2024.9.1</Version>
<Authors>Александр Бабаев</Authors>
<Product>Набор компонентов ANB Software для ASP.NET Core</Product>
<Description>Библиотека полезных методов языка C# для ASP.NET Core</Description>
@@ -20,20 +20,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.843" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.3" />
<PackageReference Include="HtmlSanitizer" Version="8.1.870" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.10.0" />
<PackageReference Include="Magick.NET.Core" Version="13.10.0" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\anbs_cp\anbs_cp.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Extensions\" />
</ItemGroup>
</Project>