diff --git a/src/modules/Elsa.Http/Activities/DownloadHttpFile.cs b/src/modules/Elsa.Http/Activities/DownloadHttpFile.cs index 8e62f56fd..fd8c9d0cd 100644 --- a/src/modules/Elsa.Http/Activities/DownloadHttpFile.cs +++ b/src/modules/Elsa.Http/Activities/DownloadHttpFile.cs @@ -46,7 +46,7 @@ public class DownloadHttpFile : Activity, IActivityPropertyDefaultValu UIHint = InputUIHints.DropDown )] public Input Method { get; set; } = new("GET"); - + /// /// A list of expected status codes to handle. /// @@ -102,7 +102,7 @@ public class DownloadHttpFile : Activity, IActivityPropertyDefaultValu /// [Output(IsSerializable = false)] public Output Response { get; set; } = default!; - + /// /// The HTTP response status code /// @@ -110,17 +110,23 @@ public class DownloadHttpFile : Activity, IActivityPropertyDefaultValu public Output StatusCode { get; set; } = default!; /// - /// The parsed content, if any. + /// The downloaded content stream, if any. /// - [Output(Description = "The parsed content, if any.", IsSerializable = false)] - public Output ResponseContent { get; set; } = default!; + [Output(Description = "The downloaded content stream, if any.", IsSerializable = false)] + public Output ResponseContentStream { get; set; } = default!; + + /// + /// The downloaded content bytes, if any. + /// + [Output(Description = "The downloaded content bytes, if any.", IsSerializable = false)] + public Output ResponseContentBytes { get; set; } = default!; /// /// The response headers that were received. /// [Output(Description = "The response headers that were received.")] public Output ResponseHeaders { get; set; } = default!; - + /// /// The response content headers that were received. /// @@ -150,11 +156,12 @@ public class DownloadHttpFile : Activity, IActivityPropertyDefaultValu var responseContentHeaders = new HttpHeaders(response.Content.Headers); context.Set(Response, response); - context.Set(ResponseContent, file?.Stream); + context.Set(ResponseContentStream, file?.Stream); context.Set(Result, file); context.Set(StatusCode, statusCode); context.Set(ResponseHeaders, responseHeaders); context.Set(ResponseContentHeaders, responseContentHeaders); + if (ResponseContentBytes.HasTarget(context)) context.Set(ResponseContentBytes, file?.GetBytes()); await HandleResponseAsync(context, response); } @@ -179,7 +186,7 @@ public class DownloadHttpFile : Activity, IActivityPropertyDefaultValu await HandleTaskCanceledExceptionAsync(context, e); } } - + /// /// Handles the response. /// @@ -272,11 +279,14 @@ public class DownloadHttpFile : Activity, IActivityPropertyDefaultValu var parsedContentType = new System.Net.Mime.ContentType(contentType); return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == parsedContentType.MediaType)) ?? new JsonContentFactory(); } - + object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property) { if (property.Name == nameof(ExpectedStatusCodes)) - return new List { 200 }; + return new List + { + 200 + }; return default!; } diff --git a/src/modules/Elsa.Http/DownloadableContentHandlers/MultiDownloadableContentHandler.cs b/src/modules/Elsa.Http/DownloadableContentHandlers/MultiDownloadableContentHandler.cs index 346b721cf..944e7c776 100644 --- a/src/modules/Elsa.Http/DownloadableContentHandlers/MultiDownloadableContentHandler.cs +++ b/src/modules/Elsa.Http/DownloadableContentHandlers/MultiDownloadableContentHandler.cs @@ -11,7 +11,7 @@ namespace Elsa.Http.DownloadableContentHandlers; public class MultiDownloadableContentHandler : DownloadableContentHandlerBase { /// - public override bool GetSupportsContent(object content) => content is IEnumerable enumerable and not string; + public override bool GetSupportsContent(object content) => content is IEnumerable and not string and not byte[]; /// protected override IEnumerable>> GetDownloadablesAsync(DownloadableContext context) diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 2ec04580b..57e76dc51 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -219,6 +219,10 @@ public class HttpFeature : FeatureBase { options.AddTypeAlias("FormFile"); options.AddTypeAlias("FormFile[]"); + options.AddTypeAlias("HttpFile"); + options.AddTypeAlias("HttpFile[]"); + options.AddTypeAlias("Downloadable"); + options.AddTypeAlias("Downloadable[]"); }); } } \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/ObjectConverters/ByteArrayConverter.cs b/src/modules/Elsa.JavaScript/ObjectConverters/ByteArrayConverter.cs new file mode 100644 index 000000000..a2092d1a4 --- /dev/null +++ b/src/modules/Elsa.JavaScript/ObjectConverters/ByteArrayConverter.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using Jint; +using Jint.Native; +using Jint.Runtime.Interop; + +namespace Elsa.JavaScript.ObjectConverters; + +/// +/// Converts a byte array to a instance representing a Uint8Array. +/// +internal class ByteArrayConverter : IObjectConverter +{ + public bool TryConvert(Engine engine, object value, [NotNullWhen(true)] out JsValue? result) + { + if (value is byte[] bytes) + { + // TODO: Temporary: Uint8Array creates a copy of the byte array. Instead, we want to create a view or a buffer referencing the byte array. + // See also: https://github.com/sebastienros/jint/pull/1590 + result = engine.Intrinsics.Uint8Array.Construct(bytes); + return true; + } + + result = JsValue.Null; + return false; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index be9cbae41..ac9cd3908 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -12,10 +12,13 @@ using Elsa.Extensions; using Elsa.JavaScript.Contracts; using Elsa.JavaScript.Helpers; using Elsa.JavaScript.Notifications; +using Elsa.JavaScript.ObjectConverters; using Elsa.JavaScript.Options; using Elsa.Mediator.Contracts; using Humanizer; using Jint; +using Jint.Native; +using Jint.Native.TypedArray; using Jint.Runtime.Interop; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; @@ -27,23 +30,11 @@ namespace Elsa.JavaScript.Services; /// /// Provides a JavaScript evaluator using Jint. /// -public class JintJavaScriptEvaluator : IJavaScriptEvaluator +public class JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions scriptOptions, IMemoryCache memoryCache) + : IJavaScriptEvaluator { - private readonly INotificationSender _mediator; - private readonly IMemoryCache _memoryCache; - private readonly JintOptions _jintOptions; - private readonly IConfiguration _configuration; - - /// - /// Constructor. - /// - public JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions scriptOptions, IMemoryCache memoryCache) - { - _mediator = mediator; - _memoryCache = memoryCache; - _jintOptions = scriptOptions.Value; - _configuration = configuration; - } + private readonly JintOptions _jintOptions = scriptOptions.Value; + private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions(); /// public async Task EvaluateAsync(string expression, @@ -71,13 +62,15 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator // Wrap objects in ObjectWrapper instances and set their prototype to Array.prototype if they are array-like. opts.SetWrapObjectHandler((engine, target, type) => { - var instance = new ObjectWrapper(engine, target); + var instance = ObjectWrapper.Create(engine, target); if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType())) instance.Prototype = engine.Intrinsics.Array.PrototypeObject; return instance; }); + + opts.Interop.ObjectConverters.Add(new ByteArrayConverter()); }); configureEngine?.Invoke(engine); @@ -123,7 +116,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator // Create configuration value accessor if (_jintOptions.AllowConfigurationAccess) - engine.SetValue("getConfig", (Func)(name => _configuration.GetSection(name).Value)); + engine.SetValue("getConfig", (Func)(name => configuration.GetSection(name).Value)); // Add common .NET types. engine.RegisterType(); @@ -135,7 +128,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator _jintOptions.ConfigureEngineCallback(engine, context); // Allow listeners invoked by the mediator to configure the engine. - await _mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken); + await mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken); return engine; } @@ -159,6 +152,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator } } + [RequiresUnreferencedCode("Calls Jint.Engine.SetValue(String, T)")] private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context) { var variableNames = context.GetVariableNamesInScope().ToList(); @@ -184,7 +178,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator { var cacheKey = "jint:script:" + Hash(expression); - var parsedScript = _memoryCache.GetOrCreate(cacheKey, entry => + var parsedScript = memoryCache.GetOrCreate(cacheKey, entry => { if (_jintOptions.ScriptCacheTimeout.HasValue) entry.SetAbsoluteExpiration(_jintOptions.ScriptCacheTimeout.Value); @@ -204,15 +198,19 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator } [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")] - private static string Serialize(object value) + private string Serialize(object value) + { + return JsonSerializer.Serialize(value, _jsonSerializerOptions); + } + + private static JsonSerializerOptions CreateJsonSerializerOptions() { var options = new JsonSerializerOptions { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) }; options.Converters.Add(new JsonStringEnumConverter()); - - return JsonSerializer.Serialize(value, options); + return options; } private string Hash(string input) diff --git a/src/modules/Elsa.JavaScript/Services/TypeAliasRegistry.cs b/src/modules/Elsa.JavaScript/Services/TypeAliasRegistry.cs index c345b6402..77af847c9 100644 --- a/src/modules/Elsa.JavaScript/Services/TypeAliasRegistry.cs +++ b/src/modules/Elsa.JavaScript/Services/TypeAliasRegistry.cs @@ -24,10 +24,14 @@ public class TypeAliasRegistry : ITypeAliasRegistry this.RegisterType("Decimal"); this.RegisterType("Single"); this.RegisterType("Double"); + this.RegisterType("Buffer"); + this.RegisterType("Stream"); + this.RegisterType("Guid"); this.RegisterType("Date"); this.RegisterType("Date"); this.RegisterType("Date"); this.RegisterType("Date"); + this.RegisterType>("ObjectDictionary"); } /// diff --git a/src/modules/Elsa.Workflows.Core/Extensions/OutputExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/OutputExtensions.cs index c54bff7e7..70573c4d2 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/OutputExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/OutputExtensions.cs @@ -53,4 +53,13 @@ public static class OutputExtensions var parsedContentVariableType = (memoryBlock.Metadata as VariableBlockMetadata)?.Variable.GetType(); return parsedContentVariableType?.GenericTypeArguments.FirstOrDefault(); } + + /// + /// Returns a value indicating whether the output has a target. + /// + public static bool HasTarget(this Output? output, ActivityExecutionContext context) + { + var memoryBlockReference = output?.MemoryBlockReference(); + return memoryBlockReference is not null && context.ExpressionExecutionContext.TryGetBlock(memoryBlockReference, out _); + } } \ No newline at end of file