using System.Collections.Concurrent;
using System.ComponentModel;
using System.Web;
using AngleSharp;
using Dpz.Core.Entity.Base;
using Dpz.Core.EnumLibrary;
using Dpz.Core.Public.ViewModel.Response;
using Dpz.Core.Service.ObjectStorage.Services;
using Markdig;
using Microsoft.AspNetCore.Mvc.ModelBinding;

#pragma warning disable CS0162

namespace Dpz.Core.Web.Library;

public static class WebToolsExtensions
{
    public const string DefaultGroupId = "5e09be6ae22e4d3b7810b6c7";

    public static readonly Dictionary<int, string> Wind = new()
    {
        { 0, "静风" },
        { 1, "软风" },
        { 2, "轻风" },
        { 3, "微风" },
        { 4, "和风" },
        { 5, "清风" },
        { 6, "强风" },
        { 7, "疾风" },
        { 8, "大风" },
        { 9, "强大风" },
        { 10, "超强大风" },
        { 11, "暴风" },
        { 12, "飓风" },
    };

    /// <summary>
    /// 缓存取消令牌
    /// </summary>
    public static readonly ConcurrentDictionary<
        string,
        CancellationTokenSource
    > CancellationTokens = new();

    /// <summary>
    /// 系统启动时间
    /// </summary>
    public static readonly DateTime StartTime = new(2018, 2, 11, 13, 56, 24, DateTimeKind.Local);

    /// <summary>
    /// 设置标题
    /// </summary>
    /// <param name="controller"></param>
    /// <param name="title"></param>
    public static void SetTitle(this Controller controller, string title)
    {
        controller.ViewData["Title"] = $"{title} - 叫我阿胖";
    }

    /// <summary>
    /// 判断客户端是否期望 JSON 响应
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    public static bool ExpectsJsonResponse(this HttpRequest request)
    {
        // 检查 Accept 头,客户端期望 JSON 响应
        var accept = request.Headers.Accept.ToString();
        if (accept.Contains("application/json", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        // 检查 Content-Type,请求体是 JSON
        var contentType = request.ContentType;
        if (contentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true)
        {
            return true;
        }

        // 保持对老式 AJAX 请求的兼容性
        var with = request.Headers["X-Requested-With"];
        return string.Equals(with, "XMLHttpRequest", StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// 获取枚举描述
    /// </summary>
    /// <param name="e"></param>
    /// <returns></returns>
    public static string GetDescription(this Enum e)
    {
        var value = e.ToString();
        var attribute = e.GetType().GetField(value)?.GetCustomAttribute<DescriptionAttribute>();
        return attribute?.Description ?? "";
    }

    /// <summary>
    /// 显示文件可读大小
    /// </summary>
    /// <param name="length"></param>
    /// <returns></returns>
    public static string FileSize(this long length)
    {
        var sizeText = length + " bytes";
        if (length > 1024m && length < 1024m * 1024m * 3)
        {
            sizeText = (length / 1024m).ToString("F") + " KB";
        }
        else if (length > 1024m * 1024m * 3 && length < 1024m * 1024m * 1024m)
        {
            sizeText = (length / 1024m / 1024m).ToString("F") + " MB";
        }
        else if (length > 1024m * 1024m * 1024m)
        {
            sizeText = (length / 1024m / 1024m / 1024m).ToString("F") + " GB";
        }

        return sizeText;
    }

    /// <summary>
    /// 从markdown转成html,并从html内容中清除链接的跳转平台,并且在新标签页打开
    /// </summary>
    /// <param name="content"></param>
    /// <returns></returns>
    public static async Task<string> ClearLinkAsync(this string content)
    {
        var pipeline = new MarkdownPipelineBuilder()
            .UsePipeTables()
            .UseTaskLists()
            .UseEmphasisExtras()
            .UseAutoIdentifiers()
            .UseAdvancedExtensions()
            .DisableHtml()
            .Build();
        var detail = Markdown.ToHtml(content, pipeline);
        var context = BrowsingContext.New(Configuration.Default);
        var document = await context.OpenAsync(y => y.Content(detail));
        var images = document.GetElementsByTagName("img");
        foreach (var image in images)
        {
            var src = image.GetAttribute("src");
            image.SetAttribute("class", "lazy");
            image.SetAttribute("loading", "lazy");
            image.SetAttribute("src", $"{Program.LibraryHost}/loaders/bars.svg");
            image.SetAttribute("data-src", src ?? $"{Program.AssetsHost}/images/notfound.png");
        }

        var links = document.GetElementsByTagName("a");
        foreach (var item in links)
        {
            var href = item.GetAttribute("href");
            if (href != null)
            {
                var uri = new Uri(href);
                var parameters = HttpUtility.ParseQueryString(uri.Query);
                var target = parameters["target"];
                if (!string.IsNullOrEmpty(target))
                {
                    item.SetAttribute("href", target);
                }

                item.SetAttribute("target", "_blank");
            }
        }

        return document.Body?.InnerHtml ?? "";
    }

    public static async Task DeleteMusicAsync(
        this IObjectStorageOperation objectStorageService,
        MusicResponse musicResponse
    )
    {
        if (musicResponse == null)
        {
            throw new ArgumentNullException(nameof(musicResponse));
        }
        if (string.IsNullOrEmpty(musicResponse.MusicUrl))
        {
            throw new ArgumentException(
                "property MusicUrl is empty or null",
                nameof(musicResponse.MusicUrl)
            );
        }

        await objectStorageService.DeleteAsync(musicResponse.MusicUrl);

        if (!string.IsNullOrEmpty(musicResponse.CoverUrl))
        {
            await objectStorageService.DeleteAsync(musicResponse.CoverUrl);
        }

        if (!string.IsNullOrEmpty(musicResponse.LyricUrl))
        {
            await objectStorageService.DeleteAsync(musicResponse.LyricUrl);
        }
    }

    public static ModelStateResult CheckModelState(this ModelStateDictionary modelState)
    {
        if (modelState.IsValid)
        {
            var messages = modelState
                .SelectMany(x => x.Value?.Errors ?? new ModelErrorCollection())
                .Select(x => x.ErrorMessage)
                .Where(x => !string.IsNullOrEmpty(x))
                .ToList();
            if (messages.Count == 0)
            {
                return new ModelStateResult(false, []);
            }

            return new ModelStateResult(true, messages);
        }

        return new ModelStateResult(false, []);
    }

    private static bool TryConvertClaimValue(string raw, Type targetType, out object? value)
    {
        if (targetType == typeof(string))
        {
            value = raw;
            return true;
        }

        var underlyingType = Nullable.GetUnderlyingType(targetType);
        if (underlyingType != null)
        {
            if (string.IsNullOrWhiteSpace(raw))
            {
                value = null;
                return true;
            }

            return TryConvertClaimValue(raw, underlyingType, out value);
        }

        if (targetType.IsEnum)
        {
            if (Enum.TryParse(targetType, raw, true, out var enumValue))
            {
                value = enumValue;
                return true;
            }

            value = null;
            return false;
        }

        if (targetType == typeof(DateTime))
        {
            if (
                DateTime.TryParse(
                    raw,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.RoundtripKind,
                    out var date
                )
            )
            {
                value = date;
                return true;
            }

            if (DateTime.TryParse(raw, out date))
            {
                value = date;
                return true;
            }

            value = null;
            return false;
        }

        if (targetType == typeof(Guid))
        {
            if (Guid.TryParse(raw, out var guid))
            {
                value = guid;
                return true;
            }

            value = null;
            return false;
        }

        var converter = TypeDescriptor.GetConverter(targetType);
        if (converter.CanConvertFrom(typeof(string)))
        {
            try
            {
                value = converter.ConvertFromInvariantString(raw);
                return true;
            }
            catch
            {
                // ignored, fallback below
            }
        }

        try
        {
            value = Convert.ChangeType(raw, targetType, CultureInfo.InvariantCulture);
            return true;
        }
        catch
        {
            value = null;
            return false;
        }
    }

    extension(ClaimsPrincipal? principal)
    {
        /// <summary>
        /// 用户ID
        /// </summary>
        public string? UserId => principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        /// <summary>
        /// 严格获取用户ID
        /// <exception cref="InvalidCredentialException">如果用户未登录,将会引发此异常</exception>
        /// </summary>
        public string RequiredUserId => principal?.UserId ?? throw new InvalidCredentialException();

        /// <summary>
        /// 用户信息
        /// </summary>
        public VmUserInfo? UserInfo => principal.GetUserInfo();

        /// <summary>
        /// 严格获取用户信息
        /// <exception cref="InvalidCredentialException">如果用户未登录,将会引发此异常</exception>
        /// </summary>
        public VmUserInfo RequiredUserInfo =>
            principal?.UserInfo ?? throw new InvalidCredentialException();

        /// <summary>
        /// 权限
        /// </summary>
        public Permissions? Permissions => principal.ParsePermissions();

        /// <summary>
        /// 是否具有系统权限
        /// </summary>
        public bool HasSystem => principal.HasSystemPermission();

        private VmUserInfo? GetUserInfo()
        {
            if (principal is not { Authenticated: true })
            {
                return null;
            }

            var userInfo = VmUserInfo.Empty();
            var writeableProperties = VmUserInfo.GetCanWriteProperties();

            foreach (var claims in principal.Claims)
            {
                if (!writeableProperties.TryGetValue(claims.Type, out var property))
                {
                    continue;
                }

                var setter = PropertyAccessor.GetSetter<VmUserInfo>(property.Name);
                if (setter == null)
                {
                    continue;
                }

                if (
                    !TryConvertClaimValue(
                        claims.Value,
                        property.PropertyType,
                        out var convertedValue
                    )
                )
                {
                    continue;
                }

                setter(userInfo, convertedValue);
            }

            return userInfo;
        }

        private Permissions? ParsePermissions()
        {
            var permissionClaim = principal?.FindFirst("Permissions")?.Value;
            if (permissionClaim is null)
            {
                return null;
            }
            if (Enum.TryParse<Permissions>(permissionClaim, ignoreCase: true, out var permissions))
            {
                return permissions;
            }
            return null;
        }

        private bool HasSystemPermission()
        {
            var permissions = principal?.ParsePermissions();
            return permissions.HasValue
                && (permissions.Value & Permissions.System) == Permissions.System;
        }

        /// <summary>
        /// 是否已认证
        /// </summary>
        public bool Authenticated => principal?.Identity?.IsAuthenticated == true;
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个 ASP.NET Core Web 应用程序的工具扩展类,提供了多种实用功能。让我详细解释一下这个代码的各个部分:

主要组成部分

1. 静态常量和字典

public const string DefaultGroupId = "5e09be6ae22e4d3b7810b6c7";
public static readonly Dictionary<int, string> Wind = new() { ... };
  • 定义了默认组ID
  • 风力等级映射字典(0-12级,从静风到飓风)

2. 全局缓存和状态

public static readonly ConcurrentDictionary<string, CancellationTokenSource> CancellationTokens = new();
public static readonly DateTime StartTime = new(2018, 2, 11, 13, 56, 24, DateTimeKind.Local);
  • 线程安全的取消令牌缓存
  • 系统启动时间记录

3. 控制器扩展方法

设置页面标题

public static void SetTitle(this Controller controller, string title)
{
    controller.ViewData["Title"] = $"{title} - 叫我阿胖";
}

判断客户端是否期望JSON响应

public static bool ExpectsJsonResponse(this HttpRequest request)

通过检查 Accept 头、Content-Type 和 X-Requested-With 头来判断。

4. 通用扩展方法

枚举描述获取

public static string GetDescription(this Enum e)

通过反射获取枚举的 DescriptionAttribute 描述。

文件大小格式化

public static string FileSize(this long length)

将字节数转换为可读的文件大小格式(bytes/KB/MB/GB)。

5. Markdown 处理

public static async Task<string> ClearLinkAsync(this string content)

这是一个复杂的方法,功能包括:

  • 将 Markdown 转换为 HTML
  • 配置图片懒加载
  • 清理链接的跳转参数
  • 设置链接在新标签页打开

6. 对象存储操作

public static async Task DeleteMusicAsync(this IObjectStorageOperation objectStorageService, MusicResponse musicResponse)

删除音乐相关的文件(音乐文件、封面、歌词)。

7. 模型验证

public static ModelStateResult CheckModelState(this ModelStateDictionary modelState)

检查模型状态并返回验证结果。

8. 类型转换工具

private static bool TryConvertClaimValue(string raw, Type targetType, out object? value)

安全地将字符串转换为指定类型,支持多种类型转换。

9. ClaimsPrincipal 扩展

这是一个扩展块,为 ClaimsPrincipal 添加了多个属性和方法:

extension(ClaimsPrincipal? principal)
{
    public string? UserId => ...;
    public string RequiredUserId => ...;
    public VmUserInfo? UserInfo => ...;
    public VmUserInfo RequiredUserInfo => ...;
    public Permissions? Permissions => ...;
    public bool HasSystem => ...;
    public bool Authenticated => ...;
}

主要功能:

  • 用户ID获取:提供可选和必需的用户ID获取方式
  • 用户信息获取:从Claims中解析用户信息到 VmUserInfo 对象
  • 权限解析:解析用户权限
  • 系统权限检查:检查是否具有系统级权限
  • 认证状态检查:检查用户是否已认证

设计特点

  1. 扩展方法模式:大量使用扩展方法,提高代码的可读性和复用性
  2. 异步支持:涉及I/O操作的方法都采用异步模式
  3. 类型安全:提供了安全的类型转换和空值检查
  4. 错误处理:包含适当的异常处理和参数验证
  5. 性能考虑:使用 ConcurrentDictionary 保证线程安全

这个工具类为Web应用提供了丰富的基础功能,涵盖了用户认证、文件处理、内容转换、模型验证等多个方面。

评论加载中...