elsa-core/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs

217 lines
9.3 KiB
C#
Raw Normal View History

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;
2023-10-13 08:34:21 +00:00
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 Esprima;
using Esprima.Ast;
using Humanizer;
2022-01-12 10:43:46 +00:00
using Jint;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
2022-01-12 10:43:46 +00:00
using Microsoft.Extensions.Options;
// ReSharper disable ConvertClosureToMethodGroup
namespace Elsa.JavaScript.Services;
/// <summary>
/// Provides a JavaScript evaluator using Jint.
/// </summary>
public class JintJavaScriptEvaluator : IJavaScriptEvaluator
2022-01-12 10:43:46 +00:00
{
private readonly INotificationSender _mediator;
private readonly IMemoryCache _memoryCache;
private readonly JintOptions _jintOptions;
private readonly IConfiguration _configuration;
2022-11-30 20:52:41 +00:00
/// <summary>
/// Constructor.
2022-11-30 20:52:41 +00:00
/// </summary>
public JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> scriptOptions, IMemoryCache memoryCache)
2022-01-12 10:43:46 +00:00
{
_mediator = mediator;
_memoryCache = memoryCache;
_jintOptions = scriptOptions.Value;
_configuration = configuration;
}
2022-01-12 10:43:46 +00:00
/// <inheritdoc />
public async Task<object?> EvaluateAsync(string expression,
Type returnType,
ExpressionExecutionContext context,
ExpressionEvaluatorOptions? options = default,
Action<Engine>? configureEngine = default,
CancellationToken cancellationToken = default)
{
var engine = await GetConfiguredEngine(configureEngine, context, options, cancellationToken);
var result = ExecuteExpressionAndGetResult(engine, expression);
2023-10-13 08:34:21 +00:00
return result.ConvertTo(returnType);
}
2022-01-12 10:43:46 +00:00
private async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options, CancellationToken cancellationToken)
{
options ??= new ExpressionEvaluatorOptions();
var engine = new Engine(opts =>
2022-01-12 10:43:46 +00:00
{
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;
});
});
2022-01-12 10:43:46 +00:00
configureEngine?.Invoke(engine);
// Add common functions.
engine.SetValue("getWorkflowInstanceId", (Func<string>)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.Id));
engine.SetValue("setCorrelationId", (Action<string?>)(value => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId = value));
engine.SetValue("getCorrelationId", (Func<string?>)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId));
engine.SetValue("setVariable", (Action<string, object>)((name, value) => context.SetVariableInScope(name, value)));
engine.SetValue("getVariable", (Func<string, object?>)(name => context.GetVariableInScope(name)));
engine.SetValue("getInput", (Func<string, object?>)(name => context.GetInput(name)));
engine.SetValue("getOutputFrom", (Func<string, string?, object?>)((activityIdName, outputName) => context.GetOutput(activityIdName, outputName)));
engine.SetValue("getLastResult", (Func<object?>)(() => context.GetLastResult()));
2023-09-06 20:04:58 +00:00
// Create variable getters and setters for each variable.
CreateVariableAccessors(engine, context);
2023-09-06 20:04:58 +00:00
// 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);
2023-06-09 20:10:56 +00:00
// Create output getters for each activity.
2023-09-06 20:04:58 +00:00
CreateActivityOutputAccessors(engine, context);
// Create argument getters for each argument.
foreach (var argument in options.Arguments)
engine.SetValue($"get{argument.Key}", (Func<object?>)(() => argument.Value));
// Add common functions.
engine.SetValue("isNullOrWhiteSpace", (Func<string, bool>)(value => string.IsNullOrWhiteSpace(value)));
engine.SetValue("isNullOrEmpty", (Func<string, bool>)(value => string.IsNullOrEmpty(value)));
engine.SetValue("toJson", (Func<object, string>)(value => Serialize(value)));
engine.SetValue("parseGuid", (Func<string, Guid>)(value => Guid.Parse(value)));
engine.SetValue("newGuid", (Func<Guid>)(() => Guid.NewGuid()));
engine.SetValue("newGuidString", (Func<string>)(() => Guid.NewGuid().ToString()));
engine.SetValue("newShortGuid", (Func<string>)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "")));
2023-09-06 20:04:58 +00:00
// Deprecated, use newGuidString instead.
engine.SetValue("getGuidString", (Func<string>)(() => Guid.NewGuid().ToString()));
2023-09-06 20:04:58 +00:00
// Deprecated, use newShortGuid instead.
engine.SetValue("getShortGuid", (Func<string>)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "")));
2022-01-12 10:43:46 +00:00
// Create configuration value accessor
if (_jintOptions.AllowConfigurationAccess)
engine.SetValue("getConfig", (Func<string, object?>)(name => _configuration.GetSection(name).Value));
// Add common .NET types.
engine.RegisterType<DateTime>();
engine.RegisterType<DateTimeOffset>();
engine.RegisterType<TimeSpan>();
engine.RegisterType<Guid>();
2022-01-12 10:43:46 +00:00
// 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);
2022-01-12 10:43:46 +00:00
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<object?>)(() => input?.Value));
}
}
2023-06-09 20:10:56 +00:00
private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)
{
var variableNames = context.GetVariableNamesInScope().ToList();
foreach (var variableName in variableNames)
2022-01-12 10:43:46 +00:00
{
var pascalName = variableName.Pascalize();
engine.SetValue($"get{pascalName}", (Func<object?>)(() => context.GetVariableInScope(variableName)));
engine.SetValue($"set{pascalName}", (Action<object?>)(value => context.SetVariableInScope(variableName, value)));
}
}
2023-09-06 20:04:58 +00:00
private static void CreateActivityOutputAccessors(Engine engine, ExpressionExecutionContext context)
2023-06-09 20:10:56 +00:00
{
var activityOutputs = context.GetActivityOutputs();
foreach (var activityOutput in activityOutputs)
foreach (var outputName in activityOutput.OutputNames)
engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func<object?>)(() => context.GetOutput(activityOutput.ActivityId, outputName)));
2023-06-09 20:10:56 +00:00
}
private object? ExecuteExpressionAndGetResult(Engine engine, string expression)
{
var cacheKey = "jint:script:" + expression.GetHashCode(StringComparison.Ordinal);
var parsedScript = _memoryCache.GetOrCreate(cacheKey, entry =>
{
if (_jintOptions.ScriptCacheTimeout.HasValue)
entry.SetAbsoluteExpiration(_jintOptions.ScriptCacheTimeout.Value);
Update packages (#5286) * bumped versions to fix dependency vulnerabilities (#5256) * Update patch version in GitHub workflows The version number used in the branch checking step of the GitHub workflows has been updated. Instead of scanning for the branch containing the patch version 3.1.2, it now scans for the branch that contains version 3.1.3. This change is aligned with the updated product version. * Update git branch grep pattern in workflow file The git grep pattern has been corrected to properly identify tagged versions in the GitHub Actions workflow. The correction ensures that the workflow script fetches the right branches as per the release tag instead of patch. * Update grep command in packages workflow The grep command used in the 'packages.yml' GitHub workflow was previously looking for the exact 'refs/tags/3.1.3' string. This commit simplifies the command by making it only look for '3.1.3'. This adjustment will streamline the process and potentially prevent issues with branch recognition. * Update package versions and refactor code for Elasticsearch and JavaScript modules Updated versions of numerous packages in the Directory.Packages.props file to their latest stable releases. This includes updates to Elasticsearch, JavaScript, and MongoDB packages among others. Additionally, refactored parts of the code in the WorkflowInstanceConfiguration and JintJavaScriptEvaluator within the Elasticsearch and JavaScript modules, respectively, to improve index management and script preparation. The WorkflowInstanceStore also saw a minor adjustment. --------- Co-authored-by: Steve Taylor <stevetayloruk@users.noreply.github.com>
2024-04-26 18:07:34 +00:00
var prepareOptions = new ScriptPreparationOptions
{
ParsingOptions = new ScriptParsingOptions
{
AllowReturnOutsideFunction = true
}
};
return Engine.PrepareScript(expression, options: prepareOptions);
})!;
var result = engine.Evaluate(parsedScript);
return result.ToObject();
}
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(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);
}
2022-01-12 10:43:46 +00:00
}