diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs index 79883257d..5e7b99fd2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs @@ -7,7 +7,9 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Scripting { @@ -19,6 +21,8 @@ namespace Squidex.Domain.Apps.Core.Scripting NamedContentData Transform(ScriptContext context, string script); + Task GetAsync(ScriptContext context, string script); + bool Evaluate(string name, object context, string script); string? Interpolate(string name, object context, string script, Dictionary>? customFormatters = null); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHelpers.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHelpers.cs new file mode 100644 index 000000000..9007d0d19 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHelpers.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Globalization; +using Jint; +using Jint.Native; +using Jint.Native.Date; +using Jint.Runtime; +using Jint.Runtime.Interop; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + internal static class JintHelpers + { + public static Engine AddHelpers(this Engine engine) + { + engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify)); + engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); + engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate)); + + return engine; + } + + public static Engine AddFormatters(this Engine engine, Dictionary>? customFormatters = null) + { + if (customFormatters != null) + { + foreach (var (key, value) in customFormatters) + { + engine.SetValue(key, Safe(value)); + } + } + + engine.AddHelpers(); + + return engine; + } + + private static Func Safe(Func func) + { + return () => + { + try + { + return func(); + } + catch + { + return "null"; + } + }; + } + + private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) + { + try + { + var stringInput = TypeConverter.ToString(arguments.At(0)); + var single = false; + + if (arguments.Length > 1) + { + single = TypeConverter.ToBoolean(arguments.At(1)); + } + + return stringInput.Slugify(null, single); + } + catch + { + return JsValue.Undefined; + } + } + + private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments) + { + try + { + var dateValue = ((DateInstance)arguments.At(0)).ToDateTime(); + var dateFormat = TypeConverter.ToString(arguments.At(1)); + + return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture); + } + catch + { + return JsValue.Undefined; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHttp.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHttp.cs new file mode 100644 index 000000000..61ae14626 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHttp.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Jint; +using Jint.Native; +using Jint.Native.Json; +using Jint.Runtime; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + internal sealed class JintHttp + { + private delegate void GetJsonDelegate(string url, Action callback, JsValue? headers = null); + private readonly IHttpClientFactory httpClientFactory; + private readonly Action exceptionHandler; + private readonly CancellationToken cancellationToken; + private JsonParser parser; + + public JintHttp(IHttpClientFactory httpClientFactory, CancellationToken cancellationToken, Action exceptionHandler) + { + this.httpClientFactory = httpClientFactory; + this.exceptionHandler = exceptionHandler; + this.cancellationToken = cancellationToken; + } + + public Engine Add(Engine engine) + { + parser = new JsonParser(engine); + + engine.SetValue("getJSON", new GetJsonDelegate(GetJson)); + + return engine; + } + + private void GetJson(string url, Action callback, JsValue? headers) + { + GetJSONAsync(url, callback, headers).Forget(); + } + + private async Task GetJSONAsync(string url, Action callback, JsValue? headers) + { + try + { + using (var httpClient = httpClientFactory.CreateClient()) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + throw new ArgumentException("Url must be an absolute URL"); + } + + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + if (headers != null && headers.Type == Types.Object) + { + var obj = headers.AsObject(); + + foreach (var (key, property) in obj.GetOwnProperties()) + { + var value = TypeConverter.ToString(property.Value); + + if (!string.IsNullOrWhiteSpace(key)) + { + request.Headers.TryAddWithoutValidation(key, value ?? string.Empty); + } + } + } + + var response = await httpClient.SendAsync(request, cancellationToken); + + response.EnsureSuccessStatusCode(); + + cancellationToken.ThrowIfCancellationRequested(); + + var responseString = await response.Content.ReadAsStringAsync(); + + cancellationToken.ThrowIfCancellationRequested(); + + var responseJson = parser.Parse(responseString); + + callback(responseJson); + } + } + catch (Exception ex) + { + exceptionHandler(ex); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index dc25154fa..34539345c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -7,35 +7,44 @@ using System; using System.Collections.Generic; -using System.Globalization; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using Esprima; using Jint; using Jint.Native; -using Jint.Native.Date; -using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Interop; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; namespace Squidex.Domain.Apps.Core.Scripting { public sealed class JintScriptEngine : IScriptEngine { + private readonly IHttpClientFactory? httpClientFactory; + public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); + public JintScriptEngine(IHttpClientFactory? httpClientFactory = null) + { + this.httpClientFactory = httpClientFactory; + } + public void Execute(ScriptContext context, string script) { Guard.NotNull(context); if (!string.IsNullOrWhiteSpace(script)) { - var engine = CreateScriptEngine(context); - - EnableDisallow(engine); - EnableReject(engine); + var engine = + CreateScriptEngine() + .AddContext(context) + .AddDisallow() + .AddReject(); Execute(engine, script); } @@ -49,20 +58,11 @@ namespace Squidex.Domain.Apps.Core.Scripting if (!string.IsNullOrWhiteSpace(script)) { - var engine = CreateScriptEngine(context); - - EnableDisallow(engine); - EnableReject(engine); - - engine.SetValue("operation", new Action(() => - { - var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); - - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) - { - data.TryUpdate(out result); - } - })); + var engine = + CreateScriptEngine() + .AddContext(context) + .AddDisallow() + .AddReject(); engine.SetValue("replace", new Action(() => { @@ -90,7 +90,9 @@ namespace Squidex.Domain.Apps.Core.Scripting { try { - var engine = CreateScriptEngine(context); + var engine = + CreateScriptEngine() + .AddContext(context); engine.SetValue("replace", new Action(() => { @@ -133,46 +135,7 @@ namespace Squidex.Domain.Apps.Core.Scripting } } - private Engine CreateScriptEngine(ScriptContext context) - { - var engine = CreateScriptEngine(); - - var contextInstance = new ObjectInstance(engine); - - if (context.Data != null) - { - contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); - } - - if (context.DataOld != null) - { - contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.DataOld), true, true, true); - } - - if (context.User != null) - { - contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false); - } - - if (!string.IsNullOrWhiteSpace(context.Operation)) - { - contextInstance.FastAddProperty("operation", context.Operation, false, false, false); - } - - contextInstance.FastAddProperty("status", context.Status.ToString(), false, false, false); - - if (context.StatusOld != default) - { - contextInstance.FastAddProperty("oldStatus", context.StatusOld.ToString(), false, false, false); - } - - engine.SetValue("ctx", contextInstance); - engine.SetValue("context", contextInstance); - - return engine; - } - - private Engine CreateScriptEngine(IReferenceResolver? resolver = null, Dictionary>? customFormatters = null) + private Engine CreateScriptEngine(IReferenceResolver? resolver = null) { var engine = new Engine(options => { @@ -184,91 +147,11 @@ namespace Squidex.Domain.Apps.Core.Scripting options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance); }); - if (customFormatters != null) - { - foreach (var (key, value) in customFormatters) - { - engine.SetValue(key, Safe(value)); - } - } - - engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify)); - engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); - engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate)); + engine.AddHelpers(); return engine; } - private static Func Safe(Func func) - { - return () => - { - try - { - return func(); - } - catch - { - return "null"; - } - }; - } - - private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) - { - try - { - var stringInput = TypeConverter.ToString(arguments.At(0)); - var single = false; - - if (arguments.Length > 1) - { - single = TypeConverter.ToBoolean(arguments.At(1)); - } - - return stringInput.Slugify(null, single); - } - catch - { - return JsValue.Undefined; - } - } - - private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments) - { - try - { - var dateValue = ((DateInstance)arguments.At(0)).ToDateTime(); - var dateFormat = TypeConverter.ToString(arguments.At(1)); - - return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture); - } - catch - { - return JsValue.Undefined; - } - } - - private static void EnableDisallow(Engine engine) - { - engine.SetValue("disallow", new Action(message => - { - var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; - - throw new DomainForbiddenException(exMessage); - })); - } - - private static void EnableReject(Engine engine) - { - engine.SetValue("reject", new Action(message => - { - var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; - - throw new ValidationException("Script rejected the operation.", errors); - })); - } - public bool Evaluate(string name, object context, string script) { try @@ -293,7 +176,8 @@ namespace Squidex.Domain.Apps.Core.Scripting try { var result = - CreateScriptEngine(NullPropagation.Instance, customFormatters) + CreateScriptEngine(NullPropagation.Instance) + .AddFormatters(customFormatters) .SetValue(name, context) .Execute(script) .GetCompletionValue() @@ -308,5 +192,39 @@ namespace Squidex.Domain.Apps.Core.Scripting return ex.Message; } } + + public Task GetAsync(ScriptContext context, string script) + { + using (var cts = new CancellationTokenSource(Timeout)) + { + var tcs = new TaskCompletionSource(); + + using (cts.Token.Register(() => + { + tcs.SetCanceled(); + })) + { + var engine = + CreateScriptEngine() + .AddContext(context); + + if (httpClientFactory != null) + { + var http = new JintHttp(httpClientFactory, cts.Token, tcs.SetException); + + http.Add(engine); + } + + engine.SetValue("complete", new Action(value => + { + tcs.SetResult(JsonMapper.Map(value)); + })); + + engine.Execute(script); + } + + return tcs.Task; + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContextExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContextExtensions.cs new file mode 100644 index 000000000..c853f0bc3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContextExtensions.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint; +using Jint.Native.Object; +using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + internal static class ScriptContextExtensions + { + public static Engine AddContext(this Engine engine, ScriptContext context) + { + var contextInstance = new ObjectInstance(engine); + + if (context.Data != null) + { + contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); + } + + if (context.DataOld != null) + { + contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.DataOld), true, true, true); + } + + if (context.User != null) + { + contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false); + } + + if (!string.IsNullOrWhiteSpace(context.Operation)) + { + contextInstance.FastAddProperty("operation", context.Operation, false, false, false); + } + + contextInstance.FastAddProperty("status", context.Status.ToString(), false, false, false); + + if (context.StatusOld != default) + { + contextInstance.FastAddProperty("oldStatus", context.StatusOld.ToString(), false, false, false); + } + + engine.SetValue("ctx", contextInstance); + engine.SetValue("context", contextInstance); + + return engine; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOperations.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOperations.cs new file mode 100644 index 000000000..af6c51a1c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOperations.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + internal static class ScriptOperations + { + public static Engine AddDisallow(this Engine engine) + { + engine.SetValue("disallow", new DisallowDelegate(Disallow)); + + return engine; + } + + private delegate void DisallowDelegate(string? message); + + private static void Disallow(string? message = null) + { + message = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; + + throw new DomainForbiddenException(message); + } + + public static Engine AddReject(this Engine engine) + { + engine.SetValue("reject", new RejectDelegate(Reject)); + + return engine; + } + + private delegate void RejectDelegate(string? message); + + private static void Reject(string? message = null) + { + var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; + + throw new ValidationException("Script rejected the operation.", errors); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index dc5e48d29..c29089b08 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -17,6 +17,7 @@ + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index fa5e98504..05fb6986b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) .Returns("content-url"); - sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine()); + sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine(null)); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index 78086a64e..cc2734e61 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -6,10 +6,17 @@ // ========================================================================== using System; +using System.Linq; +using System.Net; +using System.Net.Http; using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Validation; using Xunit; @@ -18,38 +25,93 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting { public class JintScriptEngineTests { - private readonly JintScriptEngine sut = new JintScriptEngine { Timeout = TimeSpan.FromSeconds(1) }; + private readonly IHttpClientFactory httpClientFactory = A.Fake(); + private readonly JintScriptEngine sut; + + public JintScriptEngineTests() + { + sut = new JintScriptEngine(httpClientFactory) + { + Timeout = TimeSpan.FromSeconds(1) + }; + } [Fact] public void Should_throw_validation_exception_when_calling_reject() { - Assert.Throws(() => sut.Execute(new ScriptContext(), "reject()")); - Assert.Throws(() => sut.Execute(new ScriptContext(), "reject('Not valid')")); + const string script = @" + reject() + "; + + var ex = Assert.Throws(() => sut.Execute(new ScriptContext(), script)); + + Assert.Empty(ex.Errors); + } + + [Fact] + public void Should_throw_validation_exception_when_calling_reject_with_message() + { + const string script = @" + reject('Not valid') + "; + + var ex = Assert.Throws(() => sut.Execute(new ScriptContext(), script)); + + Assert.Equal("Not valid", ex.Errors.Single().Message); } [Fact] public void Should_throw_security_exception_when_calling_reject() { - Assert.Throws(() => sut.Execute(new ScriptContext(), "disallow()")); - Assert.Throws(() => sut.Execute(new ScriptContext(), "disallow('Not allowed')")); + const string script = @" + disallow() + "; + + var ex = Assert.Throws(() => sut.Execute(new ScriptContext(), script)); + + Assert.Equal("Not allowed", ex.Message); + } + + [Fact] + public void Should_throw_security_exception_when_calling_reject_with_message() + { + const string script = @" + disallow('Operation not allowed') + "; + + var ex = Assert.Throws(() => sut.Execute(new ScriptContext(), script)); + + Assert.Equal("Operation not allowed", ex.Message); } [Fact] public void Should_catch_script_syntax_errors() { - Assert.Throws(() => sut.Execute(new ScriptContext(), "invalid()")); + const string script = @" + invalid() + "; + + Assert.Throws(() => sut.Execute(new ScriptContext(), script)); } [Fact] public void Should_catch_script_runtime_errors() { - Assert.Throws(() => sut.Execute(new ScriptContext(), "throw 'Error';")); + const string script = @" + throw 'Error'; + "; + + Assert.Throws(() => sut.Execute(new ScriptContext(), script)); } [Fact] public void Should_catch_script_runtime_errors_on_execute_and_transform() { - Assert.Throws(() => sut.ExecuteAndTransform(new ScriptContext(), "throw 'Error';")); + const string script = @" + throw 'Error'; + "; + + Assert.Throws(() => sut.ExecuteAndTransform(new ScriptContext(), script)); } [Fact] @@ -58,7 +120,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var content = new NamedContentData(); var context = new ScriptContext { Data = content }; - var result = sut.Transform(context, "x => x"); + const string script = @" + x => x + "; + + var result = sut.Transform(context, script); Assert.Same(content, result); } @@ -69,7 +135,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var content = new NamedContentData(); var context = new ScriptContext { Data = content }; - Assert.Throws(() => sut.ExecuteAndTransform(context, "invalid()")); + const string script = @" + invalid(); + "; + + Assert.Throws(() => sut.ExecuteAndTransform(context, script)); } [Fact] @@ -78,7 +148,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var content = new NamedContentData(); var context = new ScriptContext { Data = content }; - var result = sut.ExecuteAndTransform(context, "var x = 0;"); + const string script = @" + var x = 0; + "; + + var result = sut.ExecuteAndTransform(context, script); Assert.Same(content, result); } @@ -96,12 +170,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var context = new ScriptContext { Data = content, Operation = "MyOperation" }; - var result = sut.ExecuteAndTransform(context, @" + const string script = @" var data = ctx.data; data.operation = { iv: ctx.operation }; - replace(data)"); + replace(data); + "; + + var result = sut.ExecuteAndTransform(context, script); Assert.Equal(expected, result); } @@ -128,7 +205,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var context = new ScriptContext { Data = content }; - var result = sut.Transform(context, @" + const string script = @" var data = ctx.data; delete data.number0; @@ -136,7 +213,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.number1.iv = data.number1.iv + 1; data.number2 = { 'iv': 10 }; - replace(data);"); + replace(data); + "; + + var result = sut.Transform(context, script); Assert.Equal(expected, result); } @@ -161,12 +241,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var context = new ScriptContext { Data = content }; - var result = sut.Transform(context, @" + const string script = @" var data = ctx.data; data.slug = { iv: slugify(data.title.iv) }; - replace(data);"); + replace(data); + "; + + var result = sut.Transform(context, script); Assert.Equal(expected, result); } @@ -191,12 +274,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var context = new ScriptContext { Data = content }; - var result = sut.Transform(context, @" + const string script = @" var data = ctx.data; data.slug = { iv: slugify(data.title.iv, true) }; - replace(data);"); + replace(data); + "; + + var result = sut.Transform(context, script); Assert.Equal(expected, result); } @@ -223,7 +309,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var context = new ScriptContext { Data = content }; - var result = sut.ExecuteAndTransform(context, @" + const string script = @" var data = ctx.data; delete data.number0; @@ -231,7 +317,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.number1.iv = data.number1.iv + 1; data.number2 = { 'iv': 10 }; - replace(data);"); + replace(data); + "; + + var result = sut.ExecuteAndTransform(context, script); Assert.Equal(expected, result); } @@ -264,10 +353,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var context = new ScriptContext { Data = content, DataOld = oldContent, User = userPrincipal }; - var result = sut.ExecuteAndTransform(context, @" + const string script = @" ctx.data.number0.iv = ctx.data.number0.iv + ctx.oldData.number0.iv * parseInt(ctx.user.id, 10); - replace(ctx.data);"); + replace(ctx.data); + "; + + var result = sut.ExecuteAndTransform(context, script); Assert.Equal(expected, result); } @@ -275,7 +367,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_evaluate_to_true_when_expression_match() { - var result = sut.Evaluate("value", new { i = 2 }, "value.i == 2"); + const string script = @" + value.i == 2 + "; + + var result = sut.Evaluate("value", new { i = 2 }, script); Assert.True(result); } @@ -283,7 +379,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_evaluate_to_true_when_status_match() { - var result = sut.Evaluate("value", new { status = Status.Published }, "value.status == 'Published'"); + const string script = @" + value.status == 'Published' + "; + + var result = sut.Evaluate("value", new { status = Status.Published }, script); Assert.True(result); } @@ -291,7 +391,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_evaluate_to_false_when_expression_match() { - var result = sut.Evaluate("value", new { i = 2 }, "value.i == 3"); + const string script = @" + value.i == 3 + "; + + var result = sut.Evaluate("value", new { i = 2 }, script); Assert.False(result); } @@ -299,9 +403,111 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_evaluate_to_false_when_script_is_invalid() { - var result = sut.Evaluate("value", new { i = 2 }, "function()"); + const string script = @" + function(); + "; + + var result = sut.Evaluate("value", new { i = 2 }, script); Assert.False(result); } + + [Fact] + public async Task Should_make_json_request() + { + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{ \"key\": 42 }") + }; + + var httpHandler = new MockupHander(httpResponse); + + A.CallTo(() => httpClientFactory.CreateClient(A._)) + .Returns(new HttpClient(httpHandler)); + + const string script = @" + getJSON('http://squidex.io', function(result) { + complete(result); + }); + "; + + var result = await sut.GetAsync(new ScriptContext(), script); + + httpHandler.ShouldBeMethod(HttpMethod.Get); + httpHandler.ShouldBeUrl("http://squidex.io/"); + + var expectedResult = JsonValue.Object().Add("key", 42); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public async Task Should_make_json_request_with_headers() + { + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{ \"key\": 42 }") + }; + + var httpHandler = new MockupHander(httpResponse); + + A.CallTo(() => httpClientFactory.CreateClient(A._)) + .Returns(new HttpClient(httpHandler)); + + const string script = @" + var headers = { + 'X-Header1': 1, + 'X-Header2': '2' + }; + + getJSON('http://squidex.io', function(result) { + complete(result); + }, headers); + "; + + var result = await sut.GetAsync(new ScriptContext(), script); + + httpHandler.ShouldBeMethod(HttpMethod.Get); + httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeHeader("X-Header1", "1"); + httpHandler.ShouldBeHeader("X-Header2", "2"); + + var expectedResult = JsonValue.Object().Add("key", 42); + + Assert.Equal(expectedResult, result); + } + + private sealed class MockupHander : HttpMessageHandler + { + private readonly HttpResponseMessage response; + private HttpRequestMessage madeRequest; + + public void ShouldBeMethod(HttpMethod method) + { + Assert.Equal(method, madeRequest.Method); + } + + public void ShouldBeUrl(string url) + { + Assert.Equal(url, madeRequest.RequestUri.ToString()); + } + + public void ShouldBeHeader(string key, string value) + { + Assert.Equal(value, madeRequest.Headers.GetValues(key).FirstOrDefault()); + } + + public MockupHander(HttpResponseMessage response) + { + this.response = response; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + madeRequest = request; + + return Task.FromResult(response); + } + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 981b22ff5..710ad4f66 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs index e6729d7bc..1b539a7eb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs @@ -267,7 +267,7 @@ namespace Squidex.Domain.Apps.Entities.Comments { var trigger = new CommentTrigger { Condition = condition }; - var handler = new CommentTriggerHandler(new JintScriptEngine(), userResolver); + var handler = new CommentTriggerHandler(new JintScriptEngine(null), userResolver); action(handler, trigger); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index c637076fc..f97a20bc3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => app.Workflows) .Returns(workflows); - sut = new DynamicContentWorkflow(new JintScriptEngine(), appProvider); + sut = new DynamicContentWorkflow(new JintScriptEngine(null), appProvider); } [Fact]