From 32007539d7c8d0f0339038a957d4fb31d68c2bba Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 24 Oct 2024 22:12:03 +0200 Subject: [PATCH] Http request jint extension. --- .../Scripting/HttpRequestJintExtension.cs | 64 +++++++ .../Config/Domain/InfrastructureServices.cs | 4 + .../HttpRequestJintExtensionTests.cs | 157 ++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 backend/src/Squidex.Web/Scripting/HttpRequestJintExtension.cs create mode 100644 backend/tests/Squidex.Web.Tests/Scripting/HttpRequestJintExtensionTests.cs diff --git a/backend/src/Squidex.Web/Scripting/HttpRequestJintExtension.cs b/backend/src/Squidex.Web/Scripting/HttpRequestJintExtension.cs new file mode 100644 index 000000000..211b064c3 --- /dev/null +++ b/backend/src/Squidex.Web/Scripting/HttpRequestJintExtension.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Core.Scripting; + +namespace Squidex.Web.Scripting; + +public sealed class HttpRequestJintExtension : IJintExtension, IScriptDescriptor +{ + private delegate RequestInfo? GetRequestDelegate(); + private readonly IHttpContextAccessor httpContextAccessor; + + private sealed class RequestInfo + { + public string Method { get; set; } + + public string Path { get; set; } + + public string? QueryString { get; set; } + + public Dictionary Query { get; set; } + } + + public HttpRequestJintExtension(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public void Extend(Engine engine) + { + var getRequest = new GetRequestDelegate(() => + { + var request = httpContextAccessor.HttpContext?.Request; + if (request == null) + { + return null; + } + + var result = new RequestInfo + { + Method = request.Method, + Path = request.Path, + Query = request.Query.ToDictionary(x => x.Key, x => x.Value.ToArray()), + QueryString = request.QueryString.Value + }; + + return result; + }); + + engine.SetValue("getRequest", getRequest); + } + + public void Describe(AddDescription describe, ScriptScope scope) + { + describe(JsonType.Function, "getRequest()", + "Gets information about the actual HTTP request."); + } +} diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index 749f18e5f..8365e8453 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -28,6 +28,7 @@ using Squidex.Pipeline.Robots; using Squidex.Shared; using Squidex.Web; using Squidex.Web.Pipeline; +using Squidex.Web.Scripting; namespace Squidex.Config.Domain; @@ -71,6 +72,9 @@ public static class InfrastructureServices services.AddSingletonAs() .As().As(); + services.AddSingletonAs() + .As().As(); + services.AddSingletonAs() .As().As(); diff --git a/backend/tests/Squidex.Web.Tests/Scripting/HttpRequestJintExtensionTests.cs b/backend/tests/Squidex.Web.Tests/Scripting/HttpRequestJintExtensionTests.cs new file mode 100644 index 000000000..9a6e16a6e --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Scripting/HttpRequestJintExtensionTests.cs @@ -0,0 +1,157 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Scripting; + +namespace Squidex.Web.Scripting; + +public class HttpRequestJintExtensionTests +{ + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly JintScriptEngine sut; + + public HttpRequestJintExtensionTests() + { + var extensions = new IJintExtension[] + { + new HttpRequestJintExtension(httpContextAccessor) + }; + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + }), extensions); + } + + [Fact] + public void Should_get_null_request() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(null); + + const string script = @" + getRequest(); + "; + + var actual = sut.Execute([], script).ToString(); + + Assert.Equal("null", actual); + } + + [Fact] + public void Should_get_method() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "DELETE"; + + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + const string script = @" + getRequest().method; + "; + + var actual = sut.Execute([], script).ToString(); + + Assert.Equal("DELETE", actual); + } + + [Fact] + public void Should_get_path() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = "/path/to/request"; + + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + const string script = @" + getRequest().path; + "; + + var actual = sut.Execute([], script).ToString(); + + Assert.Equal("/path/to/request", actual); + } + + [Fact] + public void Should_get_query_string() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = new QueryString("?key=value"); + + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + const string script = @" + getRequest().queryString; + "; + + var actual = sut.Execute([], script).ToString(); + + Assert.Equal("?key=value", actual); + } + + [Fact] + public void Should_get_query_string_if_null() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = default; + + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + const string script = @" + getRequest().queryString; + "; + + var actual = sut.Execute([], script).ToString(); + + Assert.Equal(string.Empty, actual); + } + + [Fact] + public void Should_get_query() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = new QueryString("?a=1&a=2&b=3"); + + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + const string script = @" + getRequest().query; + "; + + var actual = sut.Execute([], script).ToString(); + + Assert.Equal("{\"a\":[\"1\", \"2\"], \"b\":[\"3\"]}", actual); + } + + [Fact] + public void Should_get_concrete_query() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = new QueryString("?a=1&a=2&b=3"); + + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + const string script = @" + getRequest().query.a[1]; + "; + + var actual = sut.Execute([], script).ToString(); + + Assert.Equal("2", actual); + } +}