diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs index c08a08a61..1536ec15a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions this.urlGenerator = urlGenerator; } - public void Extend(ExecutionContext context, bool async) + public void Extend(ExecutionContext context) { context.Engine.SetValue("contentAction", new EventDelegate(() => { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs index 226a13958..acd08d133 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs @@ -15,26 +15,19 @@ using Squidex.Text; namespace Squidex.Domain.Apps.Core.Scripting { - public delegate bool ExceptionHandler(Exception exception); - - public sealed class ExecutionContext : Dictionary + public sealed class ExecutionContext : ScriptContext { - private readonly ExceptionHandler? exceptionHandler; + private Func? completion; public Engine Engine { get; } - public CancellationToken CancellationToken { get; } + public CancellationToken CancellationToken { get; private set; } public bool IsAsync { get; private set; } - internal ExecutionContext(Engine engine, CancellationToken cancellationToken, ExceptionHandler? exceptionHandler = null) - : base(StringComparer.OrdinalIgnoreCase) + internal ExecutionContext(Engine engine) { Engine = engine; - - CancellationToken = cancellationToken; - - this.exceptionHandler = exceptionHandler; } public void MarkAsync() @@ -44,10 +37,34 @@ namespace Squidex.Domain.Apps.Core.Scripting public void Fail(Exception exception) { - exceptionHandler?.Invoke(exception); + completion?.Invoke(exception); + } + + public ExecutionContext ExtendAsync(IEnumerable extensions, Func completion, CancellationToken ct) + { + CancellationToken = ct; + + this.completion = completion; + + foreach (var extension in extensions) + { + extension.ExtendAsync(this); + } + + return this; + } + + public ExecutionContext Extend(IEnumerable extensions) + { + foreach (var extension in extensions) + { + extension.Extend(this); + } + + return this; } - public void AddVariables(ScriptVars vars, ScriptOptions options) + public ExecutionContext Extend(ScriptVars vars, ScriptOptions options) { var engine = Engine; @@ -86,6 +103,8 @@ namespace Squidex.Domain.Apps.Core.Scripting } engine.SetValue("async", true); + + return this; } } } 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 1e805ae3b..8f25001ad 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 @@ -29,14 +29,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions this.httpClientFactory = httpClientFactory; } - public void Extend(ExecutionContext context, bool async) + public void ExtendAsync(ExecutionContext context) { - if (async) - { - var engine = context.Engine; + var action = new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers)); - engine.SetValue("getJSON", new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers))); - } + context.Engine.SetValue("getJSON", action); } private void GetJson(ExecutionContext context, string url, Action callback, JsValue? headers) @@ -46,6 +43,18 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions private async Task GetJsonAsync(ExecutionContext context, string url, Action callback, JsValue? headers) { + if (callback == null) + { + context.Fail(new JavaScriptException("Callback cannot be null.")); + return; + } + + if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + context.Fail(new JavaScriptException("URL is not valid.")); + return; + } + context.MarkAsync(); try diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs index 8b1a75102..4737551b3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs @@ -15,7 +15,11 @@ namespace Squidex.Domain.Apps.Core.Scripting { } - void Extend(ExecutionContext context, bool async) + void Extend(ExecutionContext context) + { + } + + void ExtendAsync(ExecutionContext context) { } } 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 b8b2912a2..69d05f9d2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -52,7 +52,11 @@ namespace Squidex.Domain.Apps.Core.Scripting using (cts.Token.Register(() => tcs.TrySetCanceled())) { - var context = CreateEngine(vars, options, tcs.TrySetException, true, cts.Token); + var context = + CreateEngine(options) + .Extend(vars, options) + .Extend(extensions) + .ExtendAsync(extensions, tcs.TrySetException, cts.Token); context.Engine.SetValue("complete", new Action(value => { @@ -82,7 +86,11 @@ namespace Squidex.Domain.Apps.Core.Scripting using (cts.Token.Register(() => tcs.TrySetCanceled())) { - var context = CreateEngine(vars, options, tcs.TrySetException, true, cts.Token); + var context = + CreateEngine(options) + .Extend(vars, options) + .Extend(extensions) + .ExtendAsync(extensions, tcs.TrySetException, cts.Token); context.Engine.SetValue("complete", new Action(_ => { @@ -126,14 +134,17 @@ namespace Squidex.Domain.Apps.Core.Scripting Guard.NotNull(vars, nameof(vars)); Guard.NotNullOrEmpty(script, nameof(script)); - var context = CreateEngine(vars, options); + var context = + CreateEngine(options) + .Extend(vars, options) + .Extend(extensions); Execute(context.Engine, script); return JsonMapper.Map(context.Engine.GetCompletionValue()); } - private ExecutionContext CreateEngine(ScriptVars vars, ScriptOptions options, ExceptionHandler? exceptionHandler = null, bool async = false, CancellationToken ct = default) + private ExecutionContext CreateEngine(ScriptOptions options) { var engine = new Engine(engineOptions => { @@ -158,14 +169,7 @@ namespace Squidex.Domain.Apps.Core.Scripting extension.Extend(engine); } - var context = new ExecutionContext(engine, ct, exceptionHandler); - - context.AddVariables(vars, options); - - foreach (var extension in extensions) - { - extension.Extend(context, async); - } + var context = new ExecutionContext(engine); return context; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs new file mode 100644 index 000000000..cde181bac --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public class ScriptContext : Dictionary + { + public ScriptContext() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out T value) + { + Guard.NotNull(key, nameof(key)); + + value = default!; + + if (TryGetValue(key, out var temp) && temp is T typed) + { + value = typed; + return true; + } + + return false; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs index ac10430e3..fa8adda24 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs @@ -6,21 +6,17 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Security.Claims; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; +#pragma warning disable CS0618 // Type or member is obsolete + namespace Squidex.Domain.Apps.Core.Scripting { - public sealed class ScriptVars : Dictionary + public sealed class ScriptVars : ScriptContext { - public ScriptVars() - : base(StringComparer.OrdinalIgnoreCase) - { - } - public ClaimsPrincipal? User { get => GetValue(); @@ -68,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Scripting get => GetValue(); set { - SetValue(value, "oldData"); + SetValue(value, nameof(OldData)); SetValue(value); } } @@ -78,11 +74,23 @@ namespace Squidex.Domain.Apps.Core.Scripting get => GetValue(); set { - SetValue(value, "oldStatus"); + SetValue(value, nameof(OldStatus)); SetValue(value); } } + [Obsolete("Use dataOld")] + public ContentData? OldData + { + get => GetValue(); + } + + [Obsolete("Use statusOld")] + public Status? OldStatus + { + get => GetValue(); + } + public void SetValue(object? value, [CallerMemberName] string? key = null) { if (key != null) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs new file mode 100644 index 000000000..d0fbdacd9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Fluid; +using Fluid.Ast; +using Fluid.Tags; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Templates; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetsFluidExtension : IFluidExtension + { + private readonly IAppProvider appProvider; + private readonly IAssetQueryService assetQuery; + + private sealed class AssetTag : ArgumentsTag + { + private readonly AssetsFluidExtension root; + + public AssetTag(AssetsFluidExtension root) + { + this.root = root; + } + + public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) + { + if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) + { + var app = await root.appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); + + if (app == null) + { + return Completion.Normal; + } + + var requestContext = + Context.Admin(app).Clone(b => b + .WithoutTotal()); + + var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue(); + + var asset = await root.assetQuery.FindAsync(requestContext, DomainId.Create(id)); + + if (asset != null) + { + var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); + + context.SetValue(name, asset); + } + } + + return Completion.Normal; + } + } + + public AssetsFluidExtension(IAppProvider appProvider, IAssetQueryService assetQuery) + { + Guard.NotNull(assetQuery, nameof(assetQuery)); + Guard.NotNull(appProvider, nameof(appProvider)); + + this.assetQuery = assetQuery; + + this.appProvider = appProvider; + } + + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + } + + public void RegisterLanguageExtensions(FluidParserFactory factory) + { + factory.RegisterTag("asset", new AssetTag(this)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs new file mode 100644 index 000000000..971ec9845 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Jint.Native; +using Jint.Runtime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetsJintExtension : IJintExtension + { + private delegate void GetAssetsDelegate(JsValue references, Action callback); + private readonly IAppProvider appProvider; + private readonly IAssetQueryService assetQuery; + + public AssetsJintExtension(IAppProvider appProvider, IAssetQueryService assetQuery) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(assetQuery, nameof(assetQuery)); + + this.appProvider = appProvider; + + this.assetQuery = assetQuery; + } + + public void ExtendAsync(ExecutionContext context) + { + if (!context.TryGetValue(nameof(ScriptVars.AppId), out var appId)) + { + return; + } + + if (!context.TryGetValue(nameof(ScriptVars.User), out var user)) + { + return; + } + + var action = new GetAssetsDelegate((references, callback) => GetReferences(context, appId, user, references, callback)); + + context.Engine.SetValue("getAsset", action); + context.Engine.SetValue("getAssets", action); + } + + private void GetReferences(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) + { + GetReferencesAsync(context, appId, user, references, callback).Forget(); + } + + private async Task GetReferencesAsync(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) + { + Guard.NotNull(callback, nameof(callback)); + + var ids = new List(); + + if (references.IsString()) + { + ids.Add(DomainId.Create(references.ToString())); + } + else if (references.IsArray()) + { + foreach (var value in references.AsArray()) + { + if (value.IsString()) + { + ids.Add(DomainId.Create(value.ToString())); + } + } + } + + if (ids.Count == 0) + { + var emptyAssets = Array.Empty(); + + callback(JsValue.FromObject(context.Engine, emptyAssets)); + return; + } + + context.MarkAsync(); + + try + { + var app = await appProvider.GetAppAsync(appId); + + if (app == null) + { + throw new JavaScriptException("App does not exist."); + } + + var requestContext = + new Context(user, app).Clone(b => b + .WithoutTotal()); + + var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids)); + + callback(JsValue.FromObject(context.Engine, assets.ToArray())); + } + catch (Exception ex) + { + context.Fail(ex); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs index e3d81eabc..e28bf645c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs @@ -24,9 +24,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter this.grainFactory = grainFactory; } - public void Extend(ExecutionContext context, bool async) + public void ExtendAsync(ExecutionContext context) { - if (context.TryGetValue("appId", out var temp) && temp is DomainId appId) + if (context.TryGetValue(nameof(ScriptVars.AppId), out var appId)) { var engine = context.Engine; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs index 6c77233c5..1d624c017 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs @@ -23,50 +23,48 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ReferencesFluidExtension : IFluidExtension { - private readonly IContentQueryService contentQueryService; private readonly IAppProvider appProvider; + private readonly IContentQueryService contentQuery; private sealed class ReferenceTag : ArgumentsTag { - private readonly IContentQueryService contentQueryService; - private readonly IAppProvider appProvider; + private readonly ReferencesFluidExtension root; - public ReferenceTag(IContentQueryService contentQueryService, IAppProvider appProvider) + public ReferenceTag(ReferencesFluidExtension root) { - this.contentQueryService = contentQueryService; - - this.appProvider = appProvider; + this.root = root; } public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) { if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) { - var app = await appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); + var app = await root.appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); if (app == null) { return Completion.Normal; } - var appContext = Context.Admin(app).Clone(b => b - .WithoutContentEnrichment() - .WithoutCleanup() - .WithUnpublished()); + var requestContext = + Context.Admin(app).Clone(b => b + .WithoutContentEnrichment() + .WithUnpublished() + .WithoutTotal()); var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue(); var domainId = DomainId.Create(id); var domainIds = new List { domainId }; - var references = await contentQueryService.QueryAsync(appContext, Q.Empty.WithIds(domainIds)); - var reference = references.FirstOrDefault(); + var contents = await root.contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds)); + var content = contents.FirstOrDefault(); - if (reference != null) + if (content != null) { var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); - context.SetValue(name, reference); + context.SetValue(name, content); } } @@ -74,12 +72,12 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - public ReferencesFluidExtension(IContentQueryService contentQueryService, IAppProvider appProvider) + public ReferencesFluidExtension(IAppProvider appProvider, IContentQueryService contentQuery) { - Guard.NotNull(contentQueryService, nameof(contentQueryService)); + Guard.NotNull(contentQuery, nameof(contentQuery)); Guard.NotNull(appProvider, nameof(appProvider)); - this.contentQueryService = contentQueryService; + this.contentQuery = contentQuery; this.appProvider = appProvider; } @@ -87,11 +85,16 @@ namespace Squidex.Domain.Apps.Entities.Contents public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) { memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); } public void RegisterLanguageExtensions(FluidParserFactory factory) { - factory.RegisterTag("reference", new ReferenceTag(contentQueryService, appProvider)); + factory.RegisterTag("reference", new ReferenceTag(this)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs new file mode 100644 index 000000000..7fc6428a3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Jint.Native; +using Jint.Runtime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ReferencesJintExtension : IJintExtension + { + private delegate void GetReferencesDelegate(JsValue references, Action callback); + private readonly IAppProvider appProvider; + private readonly IContentQueryService contentQuery; + + public ReferencesJintExtension(IAppProvider appProvider, IContentQueryService contentQuery) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(contentQuery, nameof(contentQuery)); + + this.appProvider = appProvider; + + this.contentQuery = contentQuery; + } + + public void ExtendAsync(ExecutionContext context) + { + if (!context.TryGetValue(nameof(ScriptVars.AppId), out var appId)) + { + return; + } + + if (!context.TryGetValue(nameof(ScriptVars.User), out var user)) + { + return; + } + + var action = new GetReferencesDelegate((references, callback) => GetReferences(context, appId, user, references, callback)); + + context.Engine.SetValue("getReference", action); + context.Engine.SetValue("getReferences", action); + } + + private void GetReferences(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) + { + GetReferencesAsync(context, appId, user, references, callback).Forget(); + } + + private async Task GetReferencesAsync(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) + { + Guard.NotNull(callback, nameof(callback)); + + var ids = new List(); + + if (references.IsString()) + { + ids.Add(DomainId.Create(references.ToString())); + } + else if (references.IsArray()) + { + foreach (var value in references.AsArray()) + { + if (value.IsString()) + { + ids.Add(DomainId.Create(value.ToString())); + } + } + } + + if (ids.Count == 0) + { + var emptyContents = Array.Empty(); + + callback(JsValue.FromObject(context.Engine, emptyContents)); + return; + } + + context.MarkAsync(); + + try + { + var app = await appProvider.GetAppAsync(appId); + + if (app == null) + { + throw new JavaScriptException("App does not exist."); + } + + var requestContext = + new Context(user, app).Clone(b => b + .WithoutContentEnrichment() + .WithUnpublished() + .WithoutTotal()); + + var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids)); + + callback(JsValue.FromObject(context.Engine, contents.ToArray())); + } + catch (Exception ex) + { + context.Fail(ex); + } + } + } +} diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index 5b92340ff..9b0571d05 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -47,9 +47,18 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); 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 9c5bc06ff..2bfd06608 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 @@ -11,6 +11,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using FakeItEasy; +using Jint.Runtime; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Scripting; @@ -195,7 +196,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting CanReject = true }; - var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptVars(), script, options)); + var vars = new ScriptVars(); + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script, options)); Assert.NotEmpty(ex.Errors); } @@ -212,7 +215,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting CanReject = true }; - var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptVars(), script, options)); + var vars = new ScriptVars(); + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script, options)); Assert.Equal("Not valid", ex.Errors.Single().Message); } @@ -229,7 +234,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting CanDisallow = true }; - var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptVars(), script, options)); + var vars = new ScriptVars(); + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script, options)); Assert.Equal("Script has forbidden the operation.", ex.Message); } @@ -246,25 +253,57 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting CanDisallow = true }; - var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptVars(), script, options)); + var vars = new ScriptVars(); + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script, options)); Assert.Equal("Operation not allowed", ex.Message); } + [Fact] + public async Task Should_throw_exception_when_getJson_url_is_null() + { + const string script = @" + getJSON(null, function(result) { + complete(result); + }); + "; + + var vars = new ScriptVars(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script)); + } + + [Fact] + public async Task Should_throw_exception_when_getJson_callback_is_null() + { + const string script = @" + var url = 'http://squidex.io'; + + getJSON(url, null); + "; + + var vars = new ScriptVars(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script)); + } + [Fact] public async Task Should_make_json_request() { var httpHandler = SetupRequest(); const string script = @" - async = true; + var url = 'http://squidex.io'; - getJSON('http://squidex.io', function(result) { + getJSON(url, function(result) { complete(result); }); "; - var result = await sut.ExecuteAsync(new ScriptVars(), script); + var vars = new ScriptVars(); + + var result = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Get); httpHandler.ShouldBeUrl("http://squidex.io/"); @@ -280,19 +319,21 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var httpHandler = SetupRequest(); const string script = @" - async = true; - var headers = { 'X-Header1': 1, 'X-Header2': '2' }; - getJSON('http://squidex.io', function(result) { + var url = 'http://squidex.io'; + + getJSON(url, function(result) { complete(result); }, headers); "; - var result = await sut.ExecuteAsync(new ScriptVars(), script); + var vars = new ScriptVars(); + + var result = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Get); httpHandler.ShouldBeUrl("http://squidex.io/"); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs new file mode 100644 index 000000000..3add1b614 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Templates; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetsFluidExtensionTests + { + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly FluidTemplateEngine sut; + + public AssetsFluidExtensionTests() + { + var extensions = new IFluidExtension[] + { + new AssetsFluidExtension(appProvider, assetQuery) + }; + + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + .Returns(Mocks.App(appId)); + + sut = new FluidTemplateEngine(extensions); + } + + [Fact] + public async Task Should_resolve_assets_in_loop() + { + var assetId1 = DomainId.NewGuid(); + var asset1 = CreateAsset(assetId1, 1); + var assetId2 = DomainId.NewGuid(); + var asset2 = CreateAsset(assetId2, 2); + + var @event = new EnrichedContentEvent + { + Data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(assetId1, assetId2))), + AppId = appId + }; + + A.CallTo(() => assetQuery.FindAsync(A._, assetId1, EtagVersion.Any)) + .Returns(asset1); + + A.CallTo(() => assetQuery.FindAsync(A._, assetId2, EtagVersion.Any)) + .Returns(asset2); + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" + {% for id in event.data.assets.iv %} + {% asset 'ref', id %} + Text: {{ ref.fileName }} {{ ref.id }} + {% endfor %} + "; + + var expected = $@" + Text: file1.jpg {assetId1} + Text: file2.jpg {assetId2} + "; + + var result = await sut.RenderAsync(template, vars); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index) + { + return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs new file mode 100644 index 000000000..a5229d320 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetsJintExtensionTests : IClassFixture + { + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly JintScriptEngine sut; + + public AssetsJintExtensionTests() + { + var extensions = new IJintExtension[] + { + new AssetsJintExtension(appProvider, assetQuery) + }; + + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + .Returns(Mocks.App(appId)); + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions); + } + + [Fact] + public async Task Should_resolve_asset() + { + var assetId1 = DomainId.NewGuid(); + var asset1 = CreateAsset(assetId1, 1); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(assetId1))); + + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetId1))) + .Returns(ResultList.CreateFrom(1, asset1)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getAsset(data.assets.iv[0], function (assets) { + var result1 = `Text: ${assets[0].fileName}`; + + complete(`${result1}`); + })"; + + var expected = @" + Text: file1.jpg + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_resolve_assets() + { + var assetId1 = DomainId.NewGuid(); + var asset1 = CreateAsset(assetId1, 1); + var assetId2 = DomainId.NewGuid(); + var asset2 = CreateAsset(assetId1, 2); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(assetId1, assetId2))); + + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetId1, assetId2))) + .Returns(ResultList.CreateFrom(2, asset1, asset2)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getAssets(data.assets.iv, function (assets) { + var result1 = `Text: ${assets[0].fileName}`; + var result2 = `Text: ${assets[1].fileName}`; + + complete(`${result1}\n${result2}`); + })"; + + var expected = @" + Text: file1.jpg + Text: file2.jpg + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index) + { + return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index c772187cc..1ddddda43 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -11,7 +11,6 @@ using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using FakeItEasy; -using Squidex.Caching; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs similarity index 77% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs index 01cd6b3a5..ee3acb642 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs @@ -17,18 +17,18 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Contents { - public class ReferenceFluidExtensionTests + public class ReferencesFluidExtensionTests { private readonly IContentQueryService contentQuery = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly FluidTemplateEngine sut; - public ReferenceFluidExtensionTests() + public ReferencesFluidExtensionTests() { var extensions = new IFluidExtension[] { - new ReferencesFluidExtension(contentQuery, appProvider) + new ReferencesFluidExtension(appProvider, contentQuery) }; A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var referenceId1 = DomainId.NewGuid(); var reference1 = CreateReference(referenceId1, 1); var referenceId2 = DomainId.NewGuid(); - var reference2 = CreateReference(referenceId1, 2); + var reference2 = CreateReference(referenceId2, 2); var @event = new EnrichedContentEvent { @@ -67,20 +67,20 @@ namespace Squidex.Domain.Apps.Entities.Contents }; var template = @" -{% for id in event.data.references.iv %} - {% reference 'ref', id %} - Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} -{% endfor %} -"; + {% for id in event.data.references.iv %} + {% reference 'ref', id %} + Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} {{ ref.id }} + {% endfor %} + "; - var expected = @" - Text: Hello 1 World 1 - Text: Hello 2 World 2 -"; + var expected = $@" + Text: Hello 1 World 1 {referenceId1} + Text: Hello 2 World 2 {referenceId2} + "; var result = await sut.RenderAsync(template, vars); - Assert.Equal(expected, result); + Assert.Equal(Cleanup(expected), Cleanup(result)); } private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index) @@ -98,5 +98,13 @@ namespace Squidex.Domain.Apps.Entities.Contents Id = referenceId }; } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs new file mode 100644 index 000000000..90371b471 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs @@ -0,0 +1,143 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ReferencesJintExtensionTests : IClassFixture + { + private readonly IContentQueryService contentQuery = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly JintScriptEngine sut; + + public ReferencesJintExtensionTests() + { + var extensions = new IJintExtension[] + { + new ReferencesJintExtension(appProvider, contentQuery) + }; + + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + .Returns(Mocks.App(appId)); + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions); + } + + [Fact] + public async Task Should_resolve_reference() + { + var referenceId1 = DomainId.NewGuid(); + var reference1 = CreateReference(referenceId1, 1); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(referenceId1))); + + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), A.That.HasIds(referenceId1))) + .Returns(ResultList.CreateFrom(1, reference1)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getReference(data.references.iv[0], function (references) { + var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; + + complete(`${result1}`); + })"; + + var expected = @" + Text: Hello 1 World 1 + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_resolve_references() + { + var referenceId1 = DomainId.NewGuid(); + var reference1 = CreateReference(referenceId1, 1); + var referenceId2 = DomainId.NewGuid(); + var reference2 = CreateReference(referenceId1, 2); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(referenceId1, referenceId2))); + + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), A.That.HasIds(referenceId1, referenceId2))) + .Returns(ResultList.CreateFrom(2, reference1, reference2)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getReferences(data.references.iv, function (references) { + var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; + var result2 = `Text: ${references[1].data.field1.iv} ${references[1].data.field2.iv}`; + + complete(`${result1}\n${result2}`); + })"; + + var expected = @" + Text: Hello 1 World 1 + Text: Hello 2 World 2 + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index) + { + return new ContentEntity + { + Data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddJsonValue(JsonValue.Create($"Hello {index}"))) + .AddField("field2", + new ContentFieldData() + .AddJsonValue(JsonValue.Create($"World {index}"))), + Id = referenceId + }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs index d5eecf1df..9557d774a 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -9,7 +9,6 @@ using System; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure;