diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs new file mode 100644 index 000000000..544a1894a --- /dev/null +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs @@ -0,0 +1,74 @@ +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Text.Unicode; +using Elsa.Extensions; +using Elsa.JavaScript.Notifications; +using Elsa.Mediator.Contracts; +using JetBrains.Annotations; + +namespace Elsa.JavaScript.Handlers; + +/// A handler that configures the Jint engine with common functions. +[UsedImplicitly] +public class ConfigureEngineWithCommonFunctions : INotificationHandler +{ + private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions(); + + /// + public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) + { + var engine = notification.Engine; + var context = notification.Context; + + // Add common functions. + engine.SetValue("getWorkflowDefinitionId", (Func)(() => context.GetWorkflowExecutionContext().Workflow.Identity.DefinitionId)); + engine.SetValue("getWorkflowDefinitionVersionId", (Func)(() => context.GetWorkflowExecutionContext().Workflow.Identity.Id)); + engine.SetValue("getWorkflowDefinitionVersion", (Func)(() => context.GetWorkflowExecutionContext().Workflow.Identity.Version)); + engine.SetValue("getWorkflowInstanceId", (Func)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.Id)); + engine.SetValue("setCorrelationId", (Action)(value => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId = value)); + engine.SetValue("getCorrelationId", (Func)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId)); + engine.SetValue("setVariable", (Action)((name, value) => context.SetVariableInScope(name, value))); + engine.SetValue("getVariable", (Func)(name => context.GetVariableInScope(name))); + engine.SetValue("getInput", (Func)(name => context.GetInput(name))); + engine.SetValue("getOutputFrom", (Func)((activityIdName, outputName) => context.GetOutput(activityIdName, outputName))); + engine.SetValue("getLastResult", (Func)(() => context.GetLastResult())); + engine.SetValue("isNullOrWhiteSpace", (Func)(value => string.IsNullOrWhiteSpace(value))); + engine.SetValue("isNullOrEmpty", (Func)(value => string.IsNullOrEmpty(value))); + engine.SetValue("toJson", (Func)(value => Serialize(value))); + engine.SetValue("parseGuid", (Func)(value => Guid.Parse(value))); + engine.SetValue("newGuid", (Func)(() => Guid.NewGuid())); + engine.SetValue("newGuidString", (Func)(() => Guid.NewGuid().ToString())); + engine.SetValue("newShortGuid", (Func)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""))); + engine.SetValue("bytesToString", (Func)(value => Encoding.UTF8.GetString(value))); + engine.SetValue("bytesFromString", (Func)(value => Encoding.UTF8.GetBytes(value))); + engine.SetValue("bytesToBase64", (Func)(value => Convert.ToBase64String(value))); + engine.SetValue("bytesFromBase64", (Func)(value => Convert.FromBase64String(value))); + engine.SetValue("stringToBase64", (Func)(value => Convert.ToBase64String(Encoding.UTF8.GetBytes(value)))); + engine.SetValue("stringFromBase64", (Func)(value => Encoding.UTF8.GetString(Convert.FromBase64String(value)))); + + // Deprecated, use newGuidString instead. + engine.SetValue("getGuidString", (Func)(() => Guid.NewGuid().ToString())); + + // Deprecated, use newShortGuid instead. + engine.SetValue("getShortGuid", (Func)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""))); + return Task.CompletedTask; + } + + 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 options; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonTypes.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonTypes.cs new file mode 100644 index 000000000..3443dcb60 --- /dev/null +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonTypes.cs @@ -0,0 +1,25 @@ +using Elsa.Extensions; +using Elsa.JavaScript.Notifications; +using Elsa.Mediator.Contracts; +using JetBrains.Annotations; + +namespace Elsa.JavaScript.Handlers; + +/// A handler that configures the Jint engine with common types. +[UsedImplicitly] +public class ConfigureEngineWithCommonTypes : INotificationHandler +{ + /// + public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) + { + var engine = notification.Engine; + + // Add common .NET types. + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs new file mode 100644 index 000000000..f7faa1913 --- /dev/null +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs @@ -0,0 +1,68 @@ +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Elsa.JavaScript.Notifications; +using Elsa.Mediator.Contracts; +using Humanizer; +using JetBrains.Annotations; +using Jint; + +namespace Elsa.JavaScript.Handlers; + +/// A handler that configures the Jint engine with workflow input and output accessors. +[UsedImplicitly] +public class ConfigureEngineWithVariablesAndInputOutputAccessors : INotificationHandler +{ + /// + public async Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) + { + var engine = notification.Engine; + var context = notification.Context; + + // The order of the next 3 lines is important. + CreateVariableAccessors(engine, context); + CreateWorkflowInputAccessors(engine, context); + await CreateActivityOutputAccessorsAsync(engine, context); + } + + private void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context) + { + var variableNames = context.GetVariableNamesInScope().ToList(); + + foreach (var variableName in variableNames) + { + var pascalName = variableName.Pascalize(); + engine.SetValue($"get{pascalName}", (Func)(() => context.GetVariableInScope(variableName))); + engine.SetValue($"set{pascalName}", (Action)(value => context.SetVariableInScope(variableName, value))); + } + } + + private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionContext context) + { + // Create workflow input accessors - only if the current activity is not part of a composite activity definition. + // Otherwise, the workflow input accessors will hide the composite activity input accessors which rely on variable accessors. + if (context.IsContainedWithinCompositeActivity()) + return; + + var inputs = context.GetWorkflowInputs().ToDictionary(x => x.Name); + + if (!context.TryGetWorkflowExecutionContext(out var workflowExecutionContext)) + return; + + var inputDefinitions = workflowExecutionContext.Workflow.Inputs; + + foreach (var inputDefinition in inputDefinitions) + { + var input = inputs.GetValueOrDefault(inputDefinition.Name); + engine.SetValue($"get{inputDefinition.Name}", (Func)(() => input?.Value)); + } + } + + private static async Task CreateActivityOutputAccessorsAsync(Engine engine, ExpressionExecutionContext context) + { + var activityOutputs = context.GetActivityOutputs(); + + await foreach (var activityOutput in activityOutputs) + foreach (var outputName in activityOutput.OutputNames) + engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func)(() => context.GetOutput(activityOutput.ActivityId, outputName))); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Providers/InputFunctionsDefinitionProvider.cs b/src/modules/Elsa.JavaScript/Providers/InputFunctionsDefinitionProvider.cs index 91f2a1026..080b488dc 100644 --- a/src/modules/Elsa.JavaScript/Providers/InputFunctionsDefinitionProvider.cs +++ b/src/modules/Elsa.JavaScript/Providers/InputFunctionsDefinitionProvider.cs @@ -6,12 +6,12 @@ using Elsa.Workflows.Management; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Entities; using Humanizer; +using JetBrains.Annotations; namespace Elsa.JavaScript.Providers; -/// /// Produces s for common functions. -/// +[UsedImplicitly] internal class InputFunctionsDefinitionProvider(ITypeAliasRegistry typeAliasRegistry, IWorkflowDefinitionService workflowDefinitionService) : FunctionDefinitionProvider { diff --git a/src/modules/Elsa.JavaScript/Providers/JavaScriptExpressionSyntaxProvider.cs b/src/modules/Elsa.JavaScript/Providers/JavaScriptExpressionSyntaxProvider.cs index bddfdd997..6be4ed523 100644 --- a/src/modules/Elsa.JavaScript/Providers/JavaScriptExpressionSyntaxProvider.cs +++ b/src/modules/Elsa.JavaScript/Providers/JavaScriptExpressionSyntaxProvider.cs @@ -2,10 +2,12 @@ using Elsa.Expressions.Contracts; using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.JavaScript.Expressions; +using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; namespace Elsa.JavaScript.Providers; +[UsedImplicitly] internal class JavaScriptExpressionDescriptorProvider : IExpressionDescriptorProvider { private const string TypeName = "JavaScript"; diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index c185bb8e4..3d2cbea84 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -1,24 +1,15 @@ -using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using System.Text; -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using System.Text.Unicode; using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; -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 Esprima.Ast; using Jint; -using Jint.Native; -using Jint.Native.TypedArray; using Jint.Runtime.Interop; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; @@ -34,7 +25,6 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification : IJavaScriptEvaluator { private readonly JintOptions _jintOptions = scriptOptions.Value; - private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions(); /// public async Task EvaluateAsync(string expression, @@ -56,164 +46,86 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification var engine = new Engine(opts => { - if (_jintOptions.AllowClrAccess) - opts.AllowClr(); - - // Wrap objects in ObjectWrapper instances and set their prototype to Array.prototype if they are array-like. - opts.SetWrapObjectHandler((engine, target, type) => - { - var instance = ObjectWrapper.Create(engine, target); - - if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType())) - instance.Prototype = engine.Intrinsics.Array.PrototypeObject; - - return instance; - }); - - opts.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new ExpandoObjectConverter()]); + ConfigureClrAccess(opts); + ConfigureObjectWrapper(opts); + ConfigureObjectConverters(opts); }); configureEngine?.Invoke(engine); - - // Add common functions. - engine.SetValue("getWorkflowDefinitionId", (Func)(() => context.GetWorkflowExecutionContext().Workflow.Identity.DefinitionId)); - engine.SetValue("getWorkflowDefinitionVersionId", (Func)(() => context.GetWorkflowExecutionContext().Workflow.Identity.Id)); - engine.SetValue("getWorkflowDefinitionVersion", (Func)(() => context.GetWorkflowExecutionContext().Workflow.Identity.Version)); - engine.SetValue("getWorkflowInstanceId", (Func)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.Id)); - engine.SetValue("setCorrelationId", (Action)(value => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId = value)); - engine.SetValue("getCorrelationId", (Func)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId)); - engine.SetValue("setVariable", (Action)((name, value) => context.SetVariableInScope(name, value))); - engine.SetValue("getVariable", (Func)(name => context.GetVariableInScope(name))); - engine.SetValue("getInput", (Func)(name => context.GetInput(name))); - engine.SetValue("getOutputFrom", (Func)((activityIdName, outputName) => context.GetOutput(activityIdName, outputName))); - engine.SetValue("getLastResult", (Func)(() => context.GetLastResult())); - - // Create variable getters and setters for each variable. - CreateVariableAccessors(engine, context); - - // Create workflow input accessors - only if the current activity is not part of a composite activity definition. - // Otherwise, the workflow input accessors will hide the composite activity input accessors which rely on variable accessors created above. - CreateWorkflowInputAccessors(engine, context); - - // Create output getters for each activity. - await CreateActivityOutputAccessorsAsync(engine, context); - - // Create argument getters for each argument. - foreach (var argument in options.Arguments) - engine.SetValue($"get{argument.Key}", (Func)(() => argument.Value)); - - // Add common functions. - engine.SetValue("isNullOrWhiteSpace", (Func)(value => string.IsNullOrWhiteSpace(value))); - engine.SetValue("isNullOrEmpty", (Func)(value => string.IsNullOrEmpty(value))); - engine.SetValue("toJson", (Func)(value => Serialize(value))); - engine.SetValue("parseGuid", (Func)(value => Guid.Parse(value))); - engine.SetValue("newGuid", (Func)(() => Guid.NewGuid())); - engine.SetValue("newGuidString", (Func)(() => Guid.NewGuid().ToString())); - engine.SetValue("newShortGuid", (Func)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""))); - - // Deprecated, use newGuidString instead. - engine.SetValue("getGuidString", (Func)(() => Guid.NewGuid().ToString())); - - // Deprecated, use newShortGuid instead. - engine.SetValue("getShortGuid", (Func)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""))); - - // Create configuration value accessor - if (_jintOptions.AllowConfigurationAccess) - engine.SetValue("getConfig", (Func)(name => configuration.GetSection(name).Value)); - - // Add common .NET types. - engine.RegisterType(); - engine.RegisterType(); - engine.RegisterType(); - engine.RegisterType(); - - // Invoke registered configuration callback. + ConfigureArgumentGetters(engine, options); + ConfigureConfigurationAccess(engine); _jintOptions.ConfigureEngineCallback(engine, context); - - // Allow listeners invoked by the mediator to configure the engine. await mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken); return engine; } - private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionContext context) + private void ConfigureClrAccess(Jint.Options options) { - if (context.IsInsideCompositeActivity()) - return; - - var inputs = context.GetWorkflowInputs().ToDictionary(x => x.Name); - - if (!context.TryGetWorkflowExecutionContext(out var workflowExecutionContext)) - return; - - var inputDefinitions = workflowExecutionContext.Workflow.Inputs; - - foreach (var inputDefinition in inputDefinitions) + if (_jintOptions.AllowClrAccess) + options.AllowClr(); + } + + private void ConfigureObjectWrapper(Jint.Options options) + { + options.SetWrapObjectHandler((engine, target, type) => { - var input = inputs.GetValueOrDefault(inputDefinition.Name); - engine.SetValue($"get{inputDefinition.Name}", (Func)(() => input?.Value)); - } + var instance = ObjectWrapper.Create(engine, target); + + if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType())) + instance.Prototype = engine.Intrinsics.Array.PrototypeObject; + + return instance; + }); } - [RequiresUnreferencedCode("Calls Jint.Engine.SetValue(String, T)")] - private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context) + private void ConfigureObjectConverters(Jint.Options options) { - var variableNames = context.GetVariableNamesInScope().ToList(); - - foreach (var variableName in variableNames) - { - var pascalName = variableName.Pascalize(); - engine.SetValue($"get{pascalName}", (Func)(() => context.GetVariableInScope(variableName))); - engine.SetValue($"set{pascalName}", (Action)(value => context.SetVariableInScope(variableName, value))); - } + options.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new ExpandoObjectConverter()]); } - private static async Task CreateActivityOutputAccessorsAsync(Engine engine, ExpressionExecutionContext context) + private void ConfigureArgumentGetters(Engine engine, ExpressionEvaluatorOptions options) { - var activityOutputs = context.GetActivityOutputs(); - - await foreach (var activityOutput in activityOutputs) - foreach (var outputName in activityOutput.OutputNames) - engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func)(() => context.GetOutput(activityOutput.ActivityId, outputName))); + foreach (var argument in options.Arguments) + engine.SetValue($"get{argument.Key}", (Func)(() => argument.Value)); + } + + private void ConfigureConfigurationAccess(Engine engine) + { + if (_jintOptions.AllowConfigurationAccess) + engine.SetValue("getConfig", (Func)(name => configuration.GetSection(name).Value)); } private object? ExecuteExpressionAndGetResult(Engine engine, string expression) + { + var preparedScript = GetOrCreatePrepareScript(expression); + var result = engine.Evaluate(preparedScript); + return result.ToObject(); + } + + private Prepared