From fb1ebf83cecdbaef62360ca3d968820522391d64 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 17 Nov 2024 15:06:19 +0100 Subject: [PATCH] Scripting extensions (#1141) * Custom indexes. * Adjust test. * Improve json body handling. * Just some formatting. --- .../Scripting/Extensions/HttpJintExtension.cs | 62 ++++++++++++++----- .../Scripting/JintScriptEngineHelperTests.cs | 32 ++++++++++ .../Operations/Scripting/MockupHttpHandler.cs | 2 +- frontend/src/app/theme/_forms.scss | 4 ++ 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs index 727a0006f..0683f1215 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs @@ -9,6 +9,7 @@ using System.Text; using Jint; using Jint.Native; using Jint.Native.Json; +using Jint.Native.Object; using Jint.Runtime; using Squidex.Domain.Apps.Core.Properties; using Squidex.Infrastructure; @@ -18,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions; public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor { private delegate void HttpJsonDelegate(string url, Action callback, JsValue? headers = null, bool ignoreError = false); - private delegate void HttpJsonWithBodyDelegate(string url, JsValue post, Action callback, JsValue? headers = null, bool ignoreError = false); + private delegate void HttpJsonWithBodyDelegate(string url, JsValue body, Action callback, JsValue? headers = null, bool ignoreError = false); private readonly IHttpClientFactory httpClientFactory; public HttpJintExtension(IHttpClientFactory httpClientFactory) @@ -113,34 +114,47 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor }); } - private static HttpRequestMessage CreateRequest(ScriptExecutionContext context, HttpMethod method, Uri uri, JsValue? body, JsValue? headers) + private static HttpRequestMessage CreateRequest(ScriptExecutionContext context, + HttpMethod method, + Uri uri, + JsValue? body, + JsValue? headers) { var request = new HttpRequestMessage(method, uri); - if (body != null) + var contentType = string.Empty; + + foreach (var (name, value) in GetNonEmptyProperties(headers)) { - var jsonWriter = new JsonSerializer(context.Engine); - var jsonContent = jsonWriter.Serialize(body, JsValue.Undefined, JsValue.Undefined)?.ToString(); + request.Headers.TryAddWithoutValidation(name, value ?? string.Empty); - if (jsonContent != null) + if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) { - request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + contentType = value ?? string.Empty; } } - if (headers != null && headers.Type == Types.Object) + if (body != null) { - var obj = headers.AsObject(); - - foreach (var (key, property) in obj.GetOwnProperties()) + if (string.Equals(contentType, "application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { - var value = TypeConverter.ToString(property.Value); + var formValues = new List>(); + + foreach (var (name, value) in GetNonEmptyProperties(body)) + { + formValues.Add(new (name, value)); + } - var keyString = key.AsString(); + request.Content = new FormUrlEncodedContent(formValues); + } + else + { + var jsonWriter = new JsonSerializer(context.Engine); + var jsonContent = jsonWriter.Serialize(body, JsValue.Undefined, JsValue.Undefined)?.ToString(); - if (!string.IsNullOrWhiteSpace(keyString)) + if (jsonContent != null) { - request.Headers.TryAddWithoutValidation(keyString, value ?? string.Empty); + request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); } } } @@ -185,4 +199,22 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor describe(JsonType.Function, "deleteJSON(url, callback, headers?, ignoreError?)", Resources.ScriptingDeleteJson); } + + private static IEnumerable<(string, string)> GetNonEmptyProperties(JsValue? source) + { + if (source?.IsObject() != true || source.AsObject() is not ObjectInstance obj) + { + yield break; + } + + foreach (var (key, property) in obj.GetOwnProperties()) + { + if (key.ToString() is string name && !string.IsNullOrWhiteSpace(name)) + { + var value = TypeConverter.ToString(property.Value) ?? string.Empty; + + yield return (name, value); + } + } + } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs index c6fa018f9..0619af76d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs @@ -586,6 +586,38 @@ public class JintScriptEngineHelperTests : IClassFixture Assert.Equal(expectedResult, actual); } + [Fact] + public async Task Should_make_postJson_as_form_values() + { + var httpHandler = SetupRequest(); + + var vars = new ScriptVars + { + }; + + const string script = @" + var url = 'http://squidex.io'; + + var body = { key: 42 }; + + postJSON(url, body, function(actual) { + complete(actual); + }, { + 'Content-Type': 'application/x-www-form-urlencoded' + }); + "; + + var actual = await sut.ExecuteAsync(vars, script); + + httpHandler.ShouldBeMethod(HttpMethod.Post); + httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeBody("key=42", "application/x-www-form-urlencoded"); + + var expectedResult = JsonValue.Object().Add("key", 42); + + Assert.Equal(expectedResult, actual); + } + [Fact] public async Task Should_make_putJson_request() { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs index 78234c30d..9848e6cf7 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs @@ -47,7 +47,7 @@ internal sealed class MockupHttpHandler : HttpMessageHandler currentRequest = request; - if (request.Content is StringContent body) + if (request.Content is HttpContent body) { currentContent = await body.ReadAsStringAsync(cancellationToken); currentContentType = body.Headers.ContentType?.MediaType; diff --git a/frontend/src/app/theme/_forms.scss b/frontend/src/app/theme/_forms.scss index 7e9d5c492..76a98ad21 100644 --- a/frontend/src/app/theme/_forms.scss +++ b/frontend/src/app/theme/_forms.scss @@ -114,6 +114,10 @@ ul { margin: 0; } + + li { + word-wrap: break-word; + } } .form-bubble {