using System.Diagnostics.CodeAnalysis; 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.Options; using Elsa.Mediator.Contracts; using Humanizer; using Jint; using Jint.Runtime.Interop; using Microsoft.Extensions.Options; // ReSharper disable ConvertClosureToMethodGroup namespace Elsa.JavaScript.Services; /// /// Provides a JavaScript evaluator using Jint. /// public class JintJavaScriptEvaluator : IJavaScriptEvaluator { private readonly INotificationSender _mediator; private readonly JintOptions _jintOptions; /// /// Constructor. /// public JintJavaScriptEvaluator(INotificationSender mediator, IOptions scriptOptions) { _mediator = mediator; _jintOptions = scriptOptions.Value; } /// public async Task EvaluateAsync(string expression, Type returnType, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options = default, Action? configureEngine = default, CancellationToken cancellationToken = default) { var engine = await GetConfiguredEngine(configureEngine, context, options, cancellationToken); var result = ExecuteExpressionAndGetResult(engine, expression); return result.ConvertTo(returnType); } private async Task GetConfiguredEngine(Action? configureEngine, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options, CancellationToken cancellationToken) { options ??= new ExpressionEvaluatorOptions(); 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 = new ObjectWrapper(engine, target); if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType())) instance.Prototype = engine.Intrinsics.Array.PrototypeObject; return instance; }); }); configureEngine?.Invoke(engine); // Add common functions. 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. CreateActivityOutputAccessors(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()), "[/+=]", ""))); // Add common .NET types. engine.RegisterType(); engine.RegisterType(); engine.RegisterType(); engine.RegisterType(); // Invoke registered configuration callback. _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) { 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) { var input = inputs.GetValueOrDefault(inputDefinition.Name); engine.SetValue($"get{inputDefinition.Name}", (Func)(() => input?.Value)); } } private static 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 static void CreateActivityOutputAccessors(Engine engine, ExpressionExecutionContext context) { var activityOutputs = context.GetActivityOutputs(); foreach (var activityOutput in activityOutputs) foreach (var outputName in activityOutput.OutputNames) engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func)(() => context.GetOutput(activityOutput.ActivityId, outputName))); } private static object? ExecuteExpressionAndGetResult(Engine engine, string expression) { var result = engine.Evaluate(expression); return result.ToObject(); } [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")] private static string Serialize(object value) { var options = new JsonSerializerOptions { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) }; options.Converters.Add(new JsonStringEnumConverter()); return JsonSerializer.Serialize(value, options); } }