mirror of
https://gitee.com/dotnetchina/Furion.git
synced 2025-12-06 07:49:05 +08:00
😊 新增 流变对象支持 application/x-www-form-urlencoded 表单数据进行转换
This commit is contained in:
@@ -26,8 +26,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Furion.Extensions;
|
||||
@@ -354,6 +356,120 @@ internal static partial class StringExtensions
|
||||
internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
|
||||
string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串是否是 <c>application/x-www-form-urlencoded</c> 格式
|
||||
/// </summary>
|
||||
/// <param name="output">字符串</param>
|
||||
/// <returns>
|
||||
/// <see cref="bool" />
|
||||
/// </returns>
|
||||
internal static bool IsUrlEncodedFormFormat(this string output)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(output);
|
||||
|
||||
return UrlEncodedFormFormatRegex().IsMatch(output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 <c>application/x-www-form-urlencoded</c> 格式的字符串解析为 <see cref="JsonObject" />
|
||||
/// </summary>
|
||||
/// <param name="formData">URL 编码的表单数据字符串</param>
|
||||
/// <returns>
|
||||
/// <see cref="JsonObject" />
|
||||
/// </returns>
|
||||
internal static JsonObject? ParseUrlEncodedFormToJsonObject(this string? formData)
|
||||
{
|
||||
// 尝试移除开头的 ?
|
||||
formData = formData?.TrimStart('?');
|
||||
|
||||
// 空检查
|
||||
if (string.IsNullOrWhiteSpace(formData))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 初始化 JsonObject 实例
|
||||
var root = new JsonObject();
|
||||
|
||||
// 按 & 分割每个键值对
|
||||
foreach (var part in formData.Split('&'))
|
||||
{
|
||||
// 查找第一个 =
|
||||
var eqIndex = part.IndexOf('=');
|
||||
|
||||
// 键名为空或不存在 = 则跳过
|
||||
if (eqIndex <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// URL 解码键和值
|
||||
var key = WebUtility.UrlDecode(part[..eqIndex]);
|
||||
var value = WebUtility.UrlDecode(part[(eqIndex + 1)..]);
|
||||
|
||||
// 将键名(如 user[0][name])拆分为 token:["user", "0", "name"]
|
||||
var tokens = key.Replace("]", "").Split('[');
|
||||
var current = root;
|
||||
var i = 0;
|
||||
|
||||
// 逐层构建嵌套结构
|
||||
while (i < tokens.Length)
|
||||
{
|
||||
// 空检查
|
||||
var token = tokens[i];
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否是最后一项
|
||||
if (i == tokens.Length - 1)
|
||||
{
|
||||
current[token] = value;
|
||||
break;
|
||||
}
|
||||
|
||||
// 下一项为数组索引,按数组处理
|
||||
var nextToken = tokens[i + 1];
|
||||
if (int.TryParse(nextToken, out var index) && index >= 0)
|
||||
{
|
||||
// 确保当前 token 是一个 JsonArray
|
||||
if (current[token] is not JsonArray array)
|
||||
{
|
||||
array = [];
|
||||
current[token] = array;
|
||||
}
|
||||
|
||||
// 扩容数组
|
||||
while (array.Count <= index)
|
||||
{
|
||||
array.Add(new JsonObject());
|
||||
}
|
||||
|
||||
// 进入该数组元素并跳过数组名和索引
|
||||
current = (JsonObject)array[index]!;
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 下一个 token 是普通属性名则视为对象名
|
||||
if (current[token] is not JsonObject child)
|
||||
{
|
||||
child = new JsonObject();
|
||||
current[token] = child;
|
||||
}
|
||||
|
||||
current = child;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 占位符匹配正则表达式
|
||||
/// </summary>
|
||||
@@ -376,4 +492,9 @@ internal static partial class StringExtensions
|
||||
/// </returns>
|
||||
[GeneratedRegex(@"\[\[\s*([\w\-:]+)((?:\s*\|\s*[\w\-:]+)*)\s*(?:\|\|\s*([^\]]*))?\s*\]\]")]
|
||||
private static partial Regex ConfigurationKeyRegex();
|
||||
|
||||
[GeneratedRegex(
|
||||
"^(?:(?:[a-zA-Z0-9-._~]|%[0-9A-Fa-f]{2})+=(?:[a-zA-Z0-9-._~+]|%[0-9A-Fa-f]{2})*)(?:&(?:[a-zA-Z0-9-._~]|%[0-9A-Fa-f]{2})+=(?:[a-zA-Z0-9-._~+]|%[0-9A-Fa-f]{2})*)*$",
|
||||
RegexOptions.IgnorePatternWhitespace)]
|
||||
private static partial Regex UrlEncodedFormFormatRegex();
|
||||
}
|
||||
@@ -30,7 +30,6 @@ using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using ContentDispositionHeaderValue = System.Net.Http.Headers.ContentDispositionHeaderValue;
|
||||
|
||||
namespace Furion.HttpRemote;
|
||||
@@ -155,21 +154,6 @@ internal static partial class Helpers
|
||||
return HttpMethod.Parse(httpMethod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串是否是 <c>application/x-www-form-urlencoded</c> 格式
|
||||
/// </summary>
|
||||
/// <param name="output">字符串</param>
|
||||
/// <returns>
|
||||
/// <see cref="bool" />
|
||||
/// </returns>
|
||||
internal static bool IsFormUrlEncodedFormat(string output)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(output);
|
||||
|
||||
return FormUrlEncodedFormatRegex().IsMatch(output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查 HTTP 状态码是否是重定向状态码并返回重定向时应使用的 HTTP 请求方法
|
||||
/// </summary>
|
||||
@@ -298,14 +282,4 @@ internal static partial class Helpers
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>application/x-www-form-urlencoded</c> 格式正则表达式
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <see cref="Regex" />
|
||||
/// </returns>
|
||||
[GeneratedRegex(
|
||||
@"^(?:(?:[a-zA-Z0-9-._~]+|%(?:[0-9A-Fa-f]{2}))+=(?:[a-zA-Z0-9-._~]*|%(?:[0-9A-Fa-f]{2}))+)(?:&(?:[a-zA-Z0-9-._~]+|%(?:[0-9A-Fa-f]{2}))+=(?:[a-zA-Z0-9-._~]*|%(?:[0-9A-Fa-f]{2}))+)*$")]
|
||||
private static partial Regex FormUrlEncodedFormatRegex();
|
||||
}
|
||||
@@ -24,7 +24,6 @@
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Furion.Extensions;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
@@ -49,7 +48,7 @@ public class StringContentForFormUrlEncodedContentProcessor : FormUrlEncodedCont
|
||||
}
|
||||
|
||||
// 如果原始内容是字符串类型且不是有效的 application/x-www-form-urlencoded 格式
|
||||
if (rawContent is string rawString && !Helpers.IsFormUrlEncodedFormat(rawString))
|
||||
if (rawContent is string rawString && !rawString.IsUrlEncodedFormFormat())
|
||||
{
|
||||
throw new FormatException("The content must contain only form url encoded string.");
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
// 请访问 https://gitee.com/dotnetchina/Furion 获取更多关于 Furion 项目的许可证和版权信息。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Furion.Extensions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -31,7 +30,6 @@ using Microsoft.Extensions.Options;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
using System.Web;
|
||||
|
||||
namespace Furion.Shapeless;
|
||||
|
||||
@@ -89,9 +87,7 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
|
||||
|
||||
return string.IsNullOrEmpty(json)
|
||||
? (false, null)
|
||||
: (true,
|
||||
Clay.Parse(isFormUrlEncoded ? HttpUtility.UrlDecode(json).ParseFormatKeyValueString(['&'], '?') : json,
|
||||
options));
|
||||
: (true, Clay.Parse(json, options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -255,6 +255,8 @@ public partial class Clay
|
||||
// 将对象转换为 JsonNode 实例
|
||||
var jsonNode = obj switch
|
||||
{
|
||||
// 处理 application/x-www-form-urlencoded 格式数据
|
||||
string formData when formData.IsUrlEncodedFormFormat() => formData.ParseUrlEncodedFormToJsonObject(),
|
||||
string rawJson => JsonNode.Parse(rawJson, jsonNodeOptions, jsonDocumentOptions),
|
||||
Stream utf8Json => JsonNode.Parse(utf8Json, jsonNodeOptions, jsonDocumentOptions),
|
||||
byte[] utf8JsonBytes => JsonNode.Parse(utf8JsonBytes, jsonNodeOptions, jsonDocumentOptions),
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Furion.Extensions;
|
||||
@@ -354,6 +356,120 @@ internal static partial class StringExtensions
|
||||
internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
|
||||
string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串是否是 <c>application/x-www-form-urlencoded</c> 格式
|
||||
/// </summary>
|
||||
/// <param name="output">字符串</param>
|
||||
/// <returns>
|
||||
/// <see cref="bool" />
|
||||
/// </returns>
|
||||
internal static bool IsUrlEncodedFormFormat(this string output)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(output);
|
||||
|
||||
return UrlEncodedFormFormatRegex().IsMatch(output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 <c>application/x-www-form-urlencoded</c> 格式的字符串解析为 <see cref="JsonObject" />
|
||||
/// </summary>
|
||||
/// <param name="formData">URL 编码的表单数据字符串</param>
|
||||
/// <returns>
|
||||
/// <see cref="JsonObject" />
|
||||
/// </returns>
|
||||
internal static JsonObject? ParseUrlEncodedFormToJsonObject(this string? formData)
|
||||
{
|
||||
// 尝试移除开头的 ?
|
||||
formData = formData?.TrimStart('?');
|
||||
|
||||
// 空检查
|
||||
if (string.IsNullOrWhiteSpace(formData))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 初始化 JsonObject 实例
|
||||
var root = new JsonObject();
|
||||
|
||||
// 按 & 分割每个键值对
|
||||
foreach (var part in formData.Split('&'))
|
||||
{
|
||||
// 查找第一个 =
|
||||
var eqIndex = part.IndexOf('=');
|
||||
|
||||
// 键名为空或不存在 = 则跳过
|
||||
if (eqIndex <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// URL 解码键和值
|
||||
var key = WebUtility.UrlDecode(part[..eqIndex]);
|
||||
var value = WebUtility.UrlDecode(part[(eqIndex + 1)..]);
|
||||
|
||||
// 将键名(如 user[0][name])拆分为 token:["user", "0", "name"]
|
||||
var tokens = key.Replace("]", "").Split('[');
|
||||
var current = root;
|
||||
var i = 0;
|
||||
|
||||
// 逐层构建嵌套结构
|
||||
while (i < tokens.Length)
|
||||
{
|
||||
// 空检查
|
||||
var token = tokens[i];
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否是最后一项
|
||||
if (i == tokens.Length - 1)
|
||||
{
|
||||
current[token] = value;
|
||||
break;
|
||||
}
|
||||
|
||||
// 下一项为数组索引,按数组处理
|
||||
var nextToken = tokens[i + 1];
|
||||
if (int.TryParse(nextToken, out var index) && index >= 0)
|
||||
{
|
||||
// 确保当前 token 是一个 JsonArray
|
||||
if (current[token] is not JsonArray array)
|
||||
{
|
||||
array = [];
|
||||
current[token] = array;
|
||||
}
|
||||
|
||||
// 扩容数组
|
||||
while (array.Count <= index)
|
||||
{
|
||||
array.Add(new JsonObject());
|
||||
}
|
||||
|
||||
// 进入该数组元素并跳过数组名和索引
|
||||
current = (JsonObject)array[index]!;
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 下一个 token 是普通属性名则视为对象名
|
||||
if (current[token] is not JsonObject child)
|
||||
{
|
||||
child = new JsonObject();
|
||||
current[token] = child;
|
||||
}
|
||||
|
||||
current = child;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 占位符匹配正则表达式
|
||||
/// </summary>
|
||||
@@ -376,4 +492,9 @@ internal static partial class StringExtensions
|
||||
/// </returns>
|
||||
[GeneratedRegex(@"\[\[\s*([\w\-:]+)((?:\s*\|\s*[\w\-:]+)*)\s*(?:\|\|\s*([^\]]*))?\s*\]\]")]
|
||||
private static partial Regex ConfigurationKeyRegex();
|
||||
|
||||
[GeneratedRegex(
|
||||
"^(?:(?:[a-zA-Z0-9-._~]|%[0-9A-Fa-f]{2})+=(?:[a-zA-Z0-9-._~+]|%[0-9A-Fa-f]{2})*)(?:&(?:[a-zA-Z0-9-._~]|%[0-9A-Fa-f]{2})+=(?:[a-zA-Z0-9-._~+]|%[0-9A-Fa-f]{2})*)*$",
|
||||
RegexOptions.IgnorePatternWhitespace)]
|
||||
private static partial Regex UrlEncodedFormFormatRegex();
|
||||
}
|
||||
@@ -30,7 +30,6 @@ using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using ContentDispositionHeaderValue = System.Net.Http.Headers.ContentDispositionHeaderValue;
|
||||
|
||||
namespace Furion.HttpRemote;
|
||||
@@ -155,21 +154,6 @@ internal static partial class Helpers
|
||||
return HttpMethod.Parse(httpMethod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串是否是 <c>application/x-www-form-urlencoded</c> 格式
|
||||
/// </summary>
|
||||
/// <param name="output">字符串</param>
|
||||
/// <returns>
|
||||
/// <see cref="bool" />
|
||||
/// </returns>
|
||||
internal static bool IsFormUrlEncodedFormat(string output)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(output);
|
||||
|
||||
return FormUrlEncodedFormatRegex().IsMatch(output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查 HTTP 状态码是否是重定向状态码并返回重定向时应使用的 HTTP 请求方法
|
||||
/// </summary>
|
||||
@@ -298,14 +282,4 @@ internal static partial class Helpers
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>application/x-www-form-urlencoded</c> 格式正则表达式
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <see cref="Regex" />
|
||||
/// </returns>
|
||||
[GeneratedRegex(
|
||||
@"^(?:(?:[a-zA-Z0-9-._~]+|%(?:[0-9A-Fa-f]{2}))+=(?:[a-zA-Z0-9-._~]*|%(?:[0-9A-Fa-f]{2}))+)(?:&(?:[a-zA-Z0-9-._~]+|%(?:[0-9A-Fa-f]{2}))+=(?:[a-zA-Z0-9-._~]*|%(?:[0-9A-Fa-f]{2}))+)*$")]
|
||||
private static partial Regex FormUrlEncodedFormatRegex();
|
||||
}
|
||||
@@ -24,7 +24,6 @@
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Furion.Extensions;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
@@ -49,7 +48,7 @@ public class StringContentForFormUrlEncodedContentProcessor : FormUrlEncodedCont
|
||||
}
|
||||
|
||||
// 如果原始内容是字符串类型且不是有效的 application/x-www-form-urlencoded 格式
|
||||
if (rawContent is string rawString && !Helpers.IsFormUrlEncodedFormat(rawString))
|
||||
if (rawContent is string rawString && !rawString.IsUrlEncodedFormFormat())
|
||||
{
|
||||
throw new FormatException("The content must contain only form url encoded string.");
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
// 请访问 https://gitee.com/dotnetchina/Furion 获取更多关于 Furion 项目的许可证和版权信息。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Furion.Extensions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -31,7 +30,6 @@ using Microsoft.Extensions.Options;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
using System.Web;
|
||||
|
||||
namespace Furion.Shapeless;
|
||||
|
||||
@@ -89,9 +87,7 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
|
||||
|
||||
return string.IsNullOrEmpty(json)
|
||||
? (false, null)
|
||||
: (true,
|
||||
Clay.Parse(isFormUrlEncoded ? HttpUtility.UrlDecode(json).ParseFormatKeyValueString(['&'], '?') : json,
|
||||
options));
|
||||
: (true, Clay.Parse(json, options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -255,6 +255,8 @@ public partial class Clay
|
||||
// 将对象转换为 JsonNode 实例
|
||||
var jsonNode = obj switch
|
||||
{
|
||||
// 处理 application/x-www-form-urlencoded 格式数据
|
||||
string formData when formData.IsUrlEncodedFormFormat() => formData.ParseUrlEncodedFormToJsonObject(),
|
||||
string rawJson => JsonNode.Parse(rawJson, jsonNodeOptions, jsonDocumentOptions),
|
||||
Stream utf8Json => JsonNode.Parse(utf8Json, jsonNodeOptions, jsonDocumentOptions),
|
||||
byte[] utf8JsonBytes => JsonNode.Parse(utf8JsonBytes, jsonNodeOptions, jsonDocumentOptions),
|
||||
|
||||
Reference in New Issue
Block a user