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)
- 作用:把 Markdown 转成 HTML,然后对生成的 HTML 做后处理:
- 使用 Markdig(多个扩展、禁用原始 HTML)把 Markdown 转为 HTML。
- 用 AngleSharp 解析 HTML DOM。
- 对
标签:把原始 src 移到 data-src,同时设置 class="lazy"、loading="lazy",并把 src 替换成加载占位图(Program.LibraryHost + "/loaders/bars.svg");如果原始 src 为空则使用 Program.AssetsHost 的 notfound.png。
- 对 标签:尝试读取 href 并把 href 当做 Uri 解析,然后从查询字符串里取名为 "target" 的参数(如果存在就把 href 替换为该 target),并将 target 属性设为 "_blank"(所有链接在新标签打开)。
- 注意/潜在问题:
- new Uri(href) 会对相对路径抛异常(如果 href 不是绝对 URI),应该对异常或相对 URI 做处理。
- 禁用 HTML(DisableHtml)可能会影响某些 Markdown 中的自定义 HTML 块。
- 直接把链接 target 强制为 _blank、替换 href 为 query 中的 target 参数可能带来安全/可预测性问题(需要信任输入或做白名单)。
- 把图片替换为占位然后懒加载是一种性能优化,但前端需有 JS 把 data-src 恢复到 src。
- 作用:把 Markdown 转成 HTML,然后对生成的 HTML 做后处理:
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 属性时的关键转换逻辑。
- 用于把 Claims 中的字符串值转换为目标类型,支持:
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 实现。你想先改哪部分?
评论加载中...