From 00331f7497feb08ad39cb730f53e6a8085b08406 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 21 Jan 2024 12:52:46 +0300 Subject: [PATCH] Allow multiple value processing for Form and QueryString values. (#4736) * Allow multiple value processing for Form and QueryString values. * refactor HTTPEndpoint * add multi-part form handling * cleanup * reoder form handling code --- .../Elsa.Http/Activities/HttpEndpoint.cs | 88 +++++++++++-------- .../Extensions/StringValueExtensions.cs | 28 ++++++ src/modules/Elsa.Http/Features/HttpFeature.cs | 10 +-- .../Parsers/FormHttpContentParser.cs | 28 ------ .../Parsers/StringHttpContentParser.cs | 24 ----- .../JsonWorkflowStateSerializer.cs | 16 ++-- 6 files changed, 90 insertions(+), 104 deletions(-) create mode 100644 src/modules/Elsa.Http/Extensions/StringValueExtensions.cs delete mode 100644 src/modules/Elsa.Http/Parsers/FormHttpContentParser.cs delete mode 100644 src/modules/Elsa.Http/Parsers/StringHttpContentParser.cs diff --git a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs index 1fdd7b0a1..6630d693b 100644 --- a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs +++ b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs @@ -211,8 +211,8 @@ public class HttpEndpoint : Trigger var routeData = GetRouteData(httpContext, path); var routeDictionary = routeData.Values.ToDictionary(route => route.Key, route => route.Value!); - var queryStringDictionary = httpContext.Request.Query.ToDictionary, string, object>(queryString => queryString.Key, queryString => queryString.Value[0]!); - var headersDictionary = httpContext.Request.Headers.ToDictionary, string, object>(header => header.Key, header => header.Value[0]!); + var queryStringDictionary = httpContext.Request.Query.ToObjectDictionary(); + var headersDictionary = httpContext.Request.Headers.ToObjectDictionary(); context.Set(RouteData, routeDictionary); context.Set(QueryStringData, queryStringDictionary); @@ -225,48 +225,60 @@ public class HttpEndpoint : Trigger return; } - // Read content, if any. - try + // Handle Form Fields + if (request.HasFormContentType) { - var content = await ParseContentAsync(context, request); - ParsedContent.Set(context, content); + + var formFields = request.Form.ToObjectDictionary(); + + ParsedContent.Set(context, formFields); + + // Read files, if any. + var files = ReadFilesAsync(context, request); + + if (files.Any()) + { + if (!ValidateFileSizes(context, httpContext, files)) + { + await HandleFileSizeTooLargeAsync(context, httpContext); + return; + } + + if (!ValidateFileExtensionWhitelist(context, httpContext, files)) + { + await HandleInvalidFileExtensionWhitelistAsync(context, httpContext); + return; + } + + if (!ValidateFileExtensionBlacklist(context, httpContext, files)) + { + await HandleInvalidFileExtensionBlacklistAsync(context, httpContext); + return; + } + + if (!ValidateFileMimeTypes(context, httpContext, files)) + { + await HandleInvalidFileMimeTypesAsync(context, httpContext); + return; + } + + Files.Set(context, files.ToArray()); + } } - catch (JsonException e) + else { - await HandleInvalidJsonPayloadAsync(context, httpContext, e); - throw; - } - - // Read files, if any. - var files = ReadFilesAsync(context, request); - - if (files.Any()) - { - if (!ValidateFileSizes(context, httpContext, files)) + // Parse Non-Form content. + try { - await HandleFileSizeTooLargeAsync(context, httpContext); - return; + var content = await ParseContentAsync(context, request); + ParsedContent.Set(context, content); + } + catch (JsonException e) + { + await HandleInvalidJsonPayloadAsync(context, httpContext, e); + throw; } - if (!ValidateFileExtensionWhitelist(context, httpContext, files)) - { - await HandleInvalidFileExtensionWhitelistAsync(context, httpContext); - return; - } - - if (!ValidateFileExtensionBlacklist(context, httpContext, files)) - { - await HandleInvalidFileExtensionBlacklistAsync(context, httpContext); - return; - } - - if (!ValidateFileMimeTypes(context, httpContext, files)) - { - await HandleInvalidFileMimeTypesAsync(context, httpContext); - return; - } - - Files.Set(context, files.ToArray()); } // Complete. diff --git a/src/modules/Elsa.Http/Extensions/StringValueExtensions.cs b/src/modules/Elsa.Http/Extensions/StringValueExtensions.cs new file mode 100644 index 000000000..3b7948c1e --- /dev/null +++ b/src/modules/Elsa.Http/Extensions/StringValueExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Elsa.Extensions; + +/// +/// Contains extension methods for the Enumerable dictonaries. +/// +public static class StringValueExtensions +{ + /// + /// Convert the collection to the desired dictionary type. + /// + /// + /// + public static Dictionary ToObjectDictionary(this IEnumerable> collection) + { + return collection.ToDictionary, string, object>( + item => item.Key, + item => item.Value.Count <= 1 ? + item.Value[0]! + : item.Value.ToArray()); + } +} diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 796eddc66..ece6c375c 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -158,12 +158,10 @@ public class HttpFeature : FeatureBase .AddRequestHandler() .AddNotificationHandler() - // Content parsers. - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - + // Content parsers. + .AddSingleton() + .AddSingleton() + // HTTP content factories. .AddScoped() .AddScoped() diff --git a/src/modules/Elsa.Http/Parsers/FormHttpContentParser.cs b/src/modules/Elsa.Http/Parsers/FormHttpContentParser.cs deleted file mode 100644 index 315ccc01b..000000000 --- a/src/modules/Elsa.Http/Parsers/FormHttpContentParser.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Xml.Serialization; -using Elsa.Extensions; -using Elsa.Http.Contracts; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Primitives; - -namespace Elsa.Http.Parsers; - -/// -/// Reads application/xml and text/xml content type streams. -/// -public class FormHttpContentParser : IHttpContentParser -{ - /// - public int Priority => 0; - - /// - public bool GetSupportsContentType(string contentType) => contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.InvariantCultureIgnoreCase); - - /// - public async Task ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken) - { - using var reader = new FormReader(content); - - return (await reader.ReadFormAsync(cancellationToken)).ToDictionary, string, object>(formField => formField.Key, formField => formField.Value[0]!); ; - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Parsers/StringHttpContentParser.cs b/src/modules/Elsa.Http/Parsers/StringHttpContentParser.cs deleted file mode 100644 index 23fb9caf6..000000000 --- a/src/modules/Elsa.Http/Parsers/StringHttpContentParser.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Elsa.Expressions.Helpers; -using Elsa.Http.Contracts; - -namespace Elsa.Http.Parsers; - -/// -/// Reads any content type streams as a string and attempts to convert the string to the specified return type. -/// -public class StringHttpContentParser : IHttpContentParser -{ - /// - public int Priority => -10; - - /// - public bool GetSupportsContentType(string contentType) => !contentType.StartsWith("multipart/form-data"); - - /// - public async Task ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken) - { - using var reader = new StreamReader(content, leaveOpen: true); - var text = await reader.ReadToEndAsync(); - return returnType == null ? text : text.ConvertTo(returnType)!; - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs index 8145e7a98..c09642d94 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -36,8 +36,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer { var options = GetSerializerOptions(); var serializingWorkflowState = new SerializingWorkflowState(options); - await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); - + await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); + return JsonSerializer.Serialize(workflowState, options); } @@ -46,8 +46,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer { var options = GetSerializerOptions(); var serializingWorkflowState = new SerializingWorkflowState(options); - await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); - + await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); + return JsonSerializer.SerializeToUtf8Bytes(workflowState, options); } @@ -56,8 +56,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer { var options = GetSerializerOptions(); var serializingWorkflowState = new SerializingWorkflowState(options); - await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); - + await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); + return JsonSerializer.SerializeToElement(workflowState, options); } @@ -110,8 +110,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer options.Converters.Add(JsonMetadataServices.TimeSpanConverter); options.Converters.Add(new PolymorphicObjectConverterFactory()); options.Converters.Add(new TypeJsonConverter(_wellKnownTypeRegistry)); - options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory)); - + options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory)); + return options; } } \ No newline at end of file