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 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分析的结果,请根据实际情况进行判断。

代码解释

这是一个C# ASP.NET Core应用程序的Web工具扩展类库,提供了一系列静态扩展方法和工具函数。

主要功能模块

1. 常量定义

public const string DefaultGroupId = "5e09be6ae22e4d3b7810b6c7";
public static readonly Dictionary<int, string> Wind  // 风力等级字典
public static readonly DateTime StartTime  // 系统启动时间

2. HTTP请求处理

  • ExpectsJsonResponse: 判断客户端是否期望JSON响应
    • 检查Accept头
    • 检查Content-Type
    • 兼容XMLHttpRequest(传统AJAX)

3. 字符串和格式化工具

  • GetDescription: 获取枚举的Description特性值
  • FileSize: 将字节数转换为可读的文件大小(KB/MB/GB)
  • ClearLinkAsync: Markdown转HTML并处理链接
    • 使用Markdig将Markdown转换为HTML
    • 使用AngleSharp解析HTML
    • 为图片添加懒加载属性
    • 清理链接参数并在新标签页打开

4. 对象存储操作

  • DeleteMusicAsync: 删除音乐相关文件
    • 删除音乐文件URL
    • 删除封面图片URL(可选)
    • 删除歌词文件URL(可选)

5. 模型验证

  • CheckModelState: 检查ModelState并返回验证结果
    • 提取所有错误消息
    • 返回ModelStateResult对象

6. 类型转换工具

  • TryConvertClaimValue: 尝试将字符串转换为目标类型
    • 支持基本类型、枚举、DateTime、Guid
    • 支持可空类型
    • 使用TypeConverter进行复杂类型转换

7. 用户身份扩展(Extension Syntax)

针对ClaimsPrincipal的扩展属性(使用C# 13的extension语法):

extension(ClaimsPrincipal? principal)
{
    public string? UserId           // 用户ID
    public string RequiredUserId    // 强制要求用户ID(未登录抛异常)
    public VmUserInfo? UserInfo     // 用户信息对象
    public VmUserInfo RequiredUserInfo  // 强制要求用户信息
    public Permissions? Permissions  // 用户权限
    public bool HasSystem           // 是否有系统权限
    public bool Authenticated       // 是否已认证
}

核心方法:

  • GetUserInfo: 从Claims反序列化用户信息对象

    • 遍历所有Claims
    • 通过反射设置对象属性
    • 使用PropertyAccessor提高性能
  • ParsePermissions: 解析权限枚举

  • HasSystemPermission: 检查是否具有系统级权限

技术特点

  1. 依赖注入友好: 使用扩展方法模式
  2. 异步支持: Markdown和HTML处理使用异步方法
  3. 错误处理: 参数校验和异常抛出
  4. 性能优化: 使用反射缓存(PropertyAccessor)
  5. 安全性: 链接清理、内容净化
  6. 现代C#语法: 使用了extension、nullable类型、模式匹配等

使用场景

  • Web应用的通用工具方法库
  • 用户认证和授权
  • 内容展示(Markdown博客)
  • 文件管理
  • API响应格式化
评论加载中...