using System.Collections.Concurrent;
using System.ComponentModel;
using System.Web;
using AngleSharp;
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>
    /// 是否为ajax请求
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    public static bool IsAjax(this HttpRequest request)
    {
        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 = ApplicationTools.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分析的结果,请根据实际情况进行判断。

下面按模块逐项解释这段代码在做什么、重要细节和一些可注意的问题或改进建议。

总体

  • 这是一个扩展/工具类(WebToolsExtensions),为 ASP.NET(Core)Web 项目提供了若干通用方法、常量和扩展功能(请求/控制器/枚举/ClaimsPrincipal/模型校验/文件尺寸/Markdown 处理/对象存储删除等)。
  • 依赖项包括 Markdig(Markdown → HTML)、AngleSharp(HTML 解析/修改)、System.Web.HttpUtility、以及项目内的类型(VmUserInfo、Permissions、IObjectStorageOperation、MusicResponse、ModelStateResult、Program.LibraryHost/AssetsHost 等)。

常量与字段

  • DefaultGroupId:字符串常量,默认组 ID。
  • Wind:int→string 的字典,表示风力级别到中文名的映射。
  • CancellationTokens:ConcurrentDictionary<string, CancellationTokenSource>,可用于全局缓存/共享取消令牌源。
  • StartTime:项目的启动时间常量(写死的 DateTime)。

扩展与工具方法(逐个说明)

  • SetTitle(this Controller controller, string title)

    • 将页面标题写入 controller.ViewData["Title"],格式为 "{title} - 叫我阿胖"。
  • IsAjax(this HttpRequest request)

    • 判断请求是否是 AJAX:检查请求头 X-Requested-With 是否等于 "XMLHttpRequest"(不区分大小写)。
  • GetDescription(this Enum e)

    • 从枚举字段读取 [Description] 特性并返回其 Description;若无特性返回空字符串。
  • FileSize(this long length)

    • 将字节长度格式化为人类可读字符串(bytes、KB、MB、GB)。
    • 注意:该方法中对阈值的判断基于不完全连续的区间(使用了 > 与 < 的组合,并且对于正好等于边界值的情况可能会落到 bytes),格式化字符串使用 ToString("F")(默认小数位),可能不是最直观/常见的实现。
  • ClearLinkAsync(this string content)

  • DeleteMusicAsync(this IObjectStorageOperation objectStorageService, MusicResponse musicResponse)

    • 从对象存储中删除音乐文件及其封面、歌词文件(如果对应 URL 非空)。
    • 包含输入校验(musicResponse 不可为 null,musicResponse.MusicUrl 不可为空),否则抛出 ArgumentNullException / ArgumentException。
  • CheckModelState(this ModelStateDictionary modelState)

    • 目的应为从 ModelState 提取错误信息并返回自定义 ModelStateResult。
    • 实现细节与疑点:
      • 逻辑上第一行 if (modelState.IsValid) 意味着 modelState 无错误时进入 if 分支,但代码在该分支内仍然尝试收集错误消息并根据 messages.Count 返回不同的 ModelStateResult。通常应该是 if (!modelState.IsValid) 来收集错误。该处看起来是逻辑错误 / 反转(可能导致始终返回“无错误”的结果)。
      • 返回 new ModelStateResult(false, []) 或 new ModelStateResult(true, messages) —— 需要看 ModelStateResult 的构造参数语义(第一个 bool 是是否有错误?)才能确认行为是否符合预期。但代码中这种结构值得审查和单元测试。
  • TryConvertClaimValue(string raw, Type targetType, out object? value)

    • 用于把 Claims 中的字符串值转换为目标类型,支持:
      • string、nullable(递归处理)、枚举(忽略大小写)、DateTime(先用 RoundtripKind 尝试,再用默认 TryParse)、Guid、以及使用 TypeDescriptor 或 Convert.ChangeType 的通用转换。
    • 返回是否转换成功,并把转换结果放到 out value。
    • 这是 GetUserInfo 中将 Claim 值设置到 VmUserInfo 属性时的关键转换逻辑。

ClaimsPrincipal 相关的“扩展”

  • 段落以 extension(ClaimsPrincipal? principal) { ... } 的形式定义了一组成员(UserId、RequiredUserId、UserInfo、RequiredUserInfo、Permissions、HasSystem、Authenticated)和若干私有辅助方法(GetUserInfo、ParsePermissions、HasSystemPermission)。
  • 含义/功能:
    • UserId:从 ClaimTypes.NameIdentifier 取得用户 ID(string?)。
    • RequiredUserId:如果未登录则抛 InvalidCredentialException,否则返回 UserId。
    • UserInfo:用 GetUserInfo() 从 Claims 解析出 VmUserInfo(通过 VmUserInfo.GetCanWriteProperties 找到可写属性,然后借助 ApplicationTools.GetSetter 设值,使用 TryConvertClaimValue 做类型转换)。
    • RequiredUserInfo:严格获取(未登录则抛异常)。
    • Permissions:从名为 "Permissions" 的 Claim 解析出枚举 Permissions。
    • HasSystem:判断权限中是否包含 Permissions.System。
    • Authenticated:包装 principal?.Identity?.IsAuthenticated。
  • 注意/问题:
    • 该代码块的语法不是常见的 C# 扩展方法写法。普通扩展方法通常写成静态类 + public static 返回类型 方法名(this ClaimsPrincipal? principal, ...)。这里的 "extension(...)" 可能是示意性写法或使用了某些源生成/新语法(若在当前项目中编译失败,需要改写为标准扩展类/方法)。如果目标是 C# 的扩展属性(C# 当前并不直接支持扩展属性),就需要用扩展方法替代或包装在静态类中。
    • GetUserInfo 里使用了 ApplicationTools.GetSetter 和 VmUserInfo.GetCanWriteProperties,这依赖于项目内的反射工具,需保证性能和安全(不要无条件写入任意 Claim)。
    • ParsePermissions 直接 Parse 枚举字符串,需注意大小写和未提供 Claim 的情况。
    • RequiredUserId/RequiredUserInfo 抛 InvalidCredentialException 表示未认证时的行为,调用方应捕获处理。

依赖与假设

  • Program.LibraryHost、Program.AssetsHost:ClearLinkAsync 使用了这两个值,需要在 Program 类中定义合适的静态属性。
  • IObjectStorageOperation:DeleteAsync 用于删除对象存储对象,具体实现由项目提供。
  • VmUserInfo、Permissions、ModelStateResult、MusicResponse 等类型为项目自定义类型。

可改进或需注意的点(总结)

  • CheckModelState 的逻辑疑似错误(应检查 !modelState.IsValid 来收集错误)。
  • ClearLinkAsync 对相对 URL 的处理(new Uri(href))会抛异常,应兼容相对路径或捕获异常并跳过解析;还要注意 XSS/安全与信任外部 Markdown 的风险。
  • FileSize 的区间判断有不连续处与边界问题,建议用明确的阈值与更常见的处理(例如使用 1024 的幂与 >=/ < 比较)。
  • “extension(ClaimsPrincipal? principal)” 语法需确认是否可编译,否则改为常规静态扩展方法实现。
  • TryConvertClaimValue 已做了较全面的转换支持,但仍要注意区域性/文化差异(DateTime、数字)和异常处理。

如果需要,我可以:

  • 帮你修正 CheckModelState 的逻辑并给出更健壮的实现;
  • 将 ClaimsPrincipal 那段改写为标准的静态扩展方法/类;
  • 修正 ClearLinkAsync 中对相对 URL 的处理与安全性改进示例;
  • 或者给出更可靠的 FileSize 实现。你想先改哪部分?
评论加载中...