diff --git a/NuGet.Config b/NuGet.Config index 3f0e00340..b49f363c2 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,6 +1,7 @@  + \ No newline at end of file diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj index 50361886d..8ec17c724 100644 --- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -10,13 +10,13 @@ - + - + - + - + diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 366672e5b..5c0960c6c 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -8,7 +8,7 @@ True - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 8f3bb8919..c01feff2e 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -12,6 +12,7 @@ using System.Text; using System.Text.RegularExpressions; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; @@ -21,7 +22,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules { public class RuleEventFormatter { - private const string Undefined = "UNDEFINED"; + private const string Undefined = "null"; + private const string ScriptSuffix = ")"; + private const string ScriptPrefix = "Script("; private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray(); private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray(); private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); @@ -29,13 +32,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules private readonly List<(char[] Pattern, Func Replacer)> patterns = new List<(char[] Pattern, Func Replacer)>(); private readonly IJsonSerializer jsonSerializer; private readonly IRuleUrlGenerator urlGenerator; + private readonly IScriptEngine scriptEngine; - public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator) + public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator, IScriptEngine scriptEngine) { Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); + Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(urlGenerator, nameof(urlGenerator)); this.jsonSerializer = jsonSerializer; + this.scriptEngine = scriptEngine; this.urlGenerator = urlGenerator; AddPattern("APP_ID", AppId); @@ -72,6 +78,18 @@ namespace Squidex.Domain.Apps.Core.HandleRules return text; } + if (text.StartsWith(ScriptPrefix, StringComparison.OrdinalIgnoreCase) && text.EndsWith(ScriptSuffix, StringComparison.OrdinalIgnoreCase)) + { + var script = text.Substring(ScriptPrefix.Length, text.Length - ScriptPrefix.Length - ScriptSuffix.Length); + + var customFunctions = new Dictionary> + { + ["contentUrl"] = () => ContentUrl(@event) + }; + + return scriptEngine.Interpolate("event", @event, script, customFunctions); + } + var current = text.AsSpan(); var sb = new StringBuilder(); @@ -186,7 +204,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules { if (@event is EnrichedContentEvent contentEvent) { - return contentEvent.Type.ToString().ToLowerInvariant(); + return contentEvent.Type.ToString(); } return Undefined; diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs index 6a72f9508..e75e1f0b8 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { private readonly NamedContentData contentData; private HashSet fieldsToDelete; - private Dictionary fieldProperties; + private Dictionary fieldProperties; private bool isChanged; public ContentDataObject(Engine engine, NamedContentData contentData) @@ -55,7 +55,9 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { foreach (var kvp in fieldProperties) { - if (kvp.Value.ContentField.TryUpdate(out var fieldData)) + var value = (ContentDataProperty)kvp.Value; + + if (value.ContentField.TryUpdate(out var fieldData)) { contentData[kvp.Key] = fieldData; } @@ -109,17 +111,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { EnsurePropertiesInitialized(); - foreach (var property in fieldProperties) - { - yield return new KeyValuePair(property.Key, property.Value); - } + return fieldProperties; } private void EnsurePropertiesInitialized() { if (fieldProperties == null) { - fieldProperties = new Dictionary(contentData.Count); + fieldProperties = new Dictionary(contentData.Count); foreach (var kvp in contentData) { diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs index 4ea31b9ea..b9a51cfbf 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs @@ -7,18 +7,17 @@ using Jint.Native; using Jint.Runtime; -using Jint.Runtime.Descriptors; using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { - public sealed class ContentDataProperty : PropertyDescriptor + public sealed class ContentDataProperty : CustomProperty { private readonly ContentDataObject contentData; private ContentFieldObject contentField; private JsValue value; - public override JsValue Value + protected override JsValue CustomValue { get { @@ -42,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper contentField.Put(kvp.Key, kvp.Value.Value, true); } - this.value = new JsValue(contentField); + this.value = contentField; } } } @@ -53,14 +52,13 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper } public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null) - : base(null, true, true, true) { this.contentData = contentData; this.contentField = contentField; if (contentField != null) { - value = new JsValue(contentField); + value = contentField; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs index f92963096..d6ef06266 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper private readonly ContentDataObject contentData; private readonly ContentFieldData fieldData; private HashSet valuesToDelete; - private Dictionary valueProperties; + private Dictionary valueProperties; private bool isChanged; public ContentFieldData FieldData @@ -67,9 +67,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { foreach (var kvp in valueProperties) { - if (kvp.Value.IsChanged) + var value = (ContentFieldProperty)kvp.Value; + + if (value.IsChanged) { - fieldData[kvp.Key] = kvp.Value.ContentValue; + fieldData[kvp.Key] = value.ContentValue; } } } @@ -114,17 +116,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { EnsurePropertiesInitialized(); - foreach (var property in valueProperties) - { - yield return new KeyValuePair(property.Key, property.Value); - } + return valueProperties; } private void EnsurePropertiesInitialized() { if (valueProperties == null) { - valueProperties = new Dictionary(FieldData.Count); + valueProperties = new Dictionary(FieldData.Count); foreach (var kvp in FieldData) { diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs index ee279b40a..ed5aa34d2 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs @@ -6,19 +6,18 @@ // ========================================================================== using Jint.Native; -using Jint.Runtime.Descriptors; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { - public sealed class ContentFieldProperty : PropertyDescriptor + public sealed class ContentFieldProperty : CustomProperty { private readonly ContentFieldObject contentField; private IJsonValue contentValue; private JsValue value; private bool isChanged; - public override JsValue Value + protected override JsValue CustomValue { get { @@ -49,7 +48,6 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper } public ContentFieldProperty(ContentFieldObject contentField, IJsonValue contentValue = null) - : base(null, true, true, true) { this.contentField = contentField; this.contentValue = contentValue; diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs new file mode 100644 index 000000000..bd1230537 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint.Runtime.Descriptors; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public abstract class CustomProperty : PropertyDescriptor + { + public CustomProperty() + : base(PropertyFlag.CustomJsValue) + { + Enumerable = true; + + Writable = true; + + Configurable = true; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs index ec9d52054..33e5fd891 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -27,11 +27,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper case JsonNull n: return JsValue.Null; case JsonScalar s: - return new JsValue(s.Value); + return new JsString(s.Value); case JsonScalar b: - return new JsValue(b.Value); + return new JsBoolean(b.Value); case JsonScalar b: - return new JsValue(b.Value); + return new JsNumber(b.Value); case JsonObject obj: return FromObject(obj, engine); case JsonArray arr: diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs index 265ac898e..55742c99d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Collections.Generic; using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Core.Scripting @@ -19,6 +21,6 @@ namespace Squidex.Domain.Apps.Core.Scripting bool Evaluate(string name, object context, string script); - string Interpolate(string name, object context, string script); + string Interpolate(string name, object context, string script, Dictionary> customFormatters = null); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index a1efe015f..35cc085ac 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -7,17 +7,19 @@ using System; using System.Collections.Generic; -using System.Reflection; +using System.Globalization; using System.Security.Claims; using Jint; using Jint.Native; +using Jint.Native.Date; using Jint.Native.Object; -using Jint.Parser; using Jint.Runtime; using Jint.Runtime.Interop; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Squidex.Infrastructure; +using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.Scripting { @@ -25,12 +27,38 @@ namespace Squidex.Domain.Apps.Core.Scripting { public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); - static JintScriptEngine() + public sealed class Converter : IObjectConverter { - var typeMappers = (Dictionary>)typeof(Engine).GetField("TypeMappers", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); + public Engine Engine { get; set; } - typeMappers.Add(typeof(NamedContentData), (engine, data) => new ContentDataObject(engine, (NamedContentData)data)); - typeMappers.Add(typeof(ClaimsPrincipal), (engine, data) => JintUser.Create(engine, (ClaimsPrincipal)data)); + public bool TryConvert(object value, out JsValue result) + { + result = null; + + if (value is Enum) + { + result = value.ToString(); + return true; + } + + switch (value) + { + case IUser user: + result = JintUser.Create(Engine, user); + return true; + case ClaimsPrincipal principal: + result = JintUser.Create(Engine, principal); + return true; + case Instant instant: + result = JsValue.FromObject(Engine, instant.ToDateTimeUtc()); + return true; + case NamedContentData content: + result = new ContentDataObject(Engine, content); + return true; + } + + return false; + } } public void Execute(ScriptContext context, string script) @@ -126,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Scripting { engine.Execute(script); } - catch (ParserException ex) + catch (ArgumentException ex) { throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); } @@ -138,7 +166,7 @@ namespace Squidex.Domain.Apps.Core.Scripting private Engine CreateScriptEngine(ScriptContext context) { - var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict()); + var engine = CreateScriptEngine(); var contextInstance = new ObjectInstance(engine); @@ -164,11 +192,56 @@ namespace Squidex.Domain.Apps.Core.Scripting engine.SetValue("ctx", contextInstance); engine.SetValue("context", contextInstance); - engine.SetValue("slugify", new ClrFunctionInstance(engine, Slugify)); return engine; } + private Engine CreateScriptEngine(IReferenceResolver resolver = null, Dictionary> customFormatters = null) + { + var converter = new Converter(); + + var engine = new Engine(options => + { + if (resolver != null) + { + options.SetReferencesResolver(resolver); + } + + options.TimeoutInterval(Timeout).Strict().AddObjectConverter(converter); + }); + + if (customFormatters != null) + { + foreach (var kvp in customFormatters) + { + engine.SetValue(kvp.Key, Safe(kvp.Value)); + } + } + + converter.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; + } + + private static Func Safe(Func func) + { + return new Func(() => + { + try + { + return func(); + } + catch + { + return "null"; + } + }); + } + private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) { try @@ -189,6 +262,21 @@ namespace Squidex.Domain.Apps.Core.Scripting } } + 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 => @@ -214,7 +302,7 @@ namespace Squidex.Domain.Apps.Core.Scripting try { var result = - new Engine(options => options.TimeoutInterval(Timeout).Strict()) + CreateScriptEngine(NullPropagation.Instance) .SetValue(name, context) .Execute(script) .GetCompletionValue() @@ -228,22 +316,24 @@ namespace Squidex.Domain.Apps.Core.Scripting } } - public string Interpolate(string name, object context, string script) + public string Interpolate(string name, object context, string script, Dictionary> customFormatters = null) { try { var result = - new Engine(options => options.TimeoutInterval(Timeout).Strict()) + CreateScriptEngine(NullPropagation.Instance, customFormatters) .SetValue(name, context) .Execute(script) .GetCompletionValue() .ToObject(); - return (string)result; + var converted = result.ToString(); + + return converted == "undefined" ? "null" : converted; } - catch + catch (Exception ex) { - return string.Empty; + return ex.Message; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs index f790c2739..083e028c1 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs @@ -5,11 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using Jint; using Jint.Runtime.Interop; using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.Scripting { @@ -17,6 +20,11 @@ namespace Squidex.Domain.Apps.Core.Scripting { private static readonly char[] ClaimSeparators = { '/', '.', ':' }; + public static ObjectWrapper Create(Engine engine, IUser user) + { + return CreateUser(engine, user.Id, false, user.Email, user.DisplayName(), user.Claims); + } + public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal) { var id = principal.OpenIdSubject(); @@ -28,19 +36,20 @@ namespace Squidex.Domain.Apps.Core.Scripting id = principal.OpenIdClientId(); } + var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value; + + return CreateUser(engine, id, isClient, principal.OpenIdEmail(), name, principal.Claims); + } + + private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string name, IEnumerable allClaims) + { var claims = - principal.Claims.GroupBy(x => x.Type) + allClaims.GroupBy(x => x.Type) .ToDictionary( x => x.Key.Split(ClaimSeparators).Last(), x => x.Select(y => y.Value).ToArray()); - return new ObjectWrapper(engine, new - { - Id = id, - IsClient = isClient, - Email = principal.OpenIdEmail(), - Claims = claims, - }); + return new ObjectWrapper(engine, new { id, isClient, email, name, claims }); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs new file mode 100644 index 000000000..adc3d577b --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint; +using Jint.Native; +using Jint.Runtime.Interop; +using Jint.Runtime.References; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class NullPropagation : IReferenceResolver + { + public static readonly NullPropagation Instance = new NullPropagation(); + + public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value) + { + value = reference.GetBase(); + + return true; + } + + public bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value) + { + return value.IsNull() || value.IsUndefined(); + } + + public bool TryGetCallable(Engine engine, object reference, out JsValue value) + { + value = new ClrFunctionInstance(engine, "anonymous", (thisObj, values) => thisObj); + + return true; + } + + public bool CheckCoercible(JsValue value) + { + return true; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 73e3a75a6..95b5cdc62 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -14,9 +14,9 @@ - - - + + + diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index 1b1b62890..23096e86d 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 227f17c93..58839d32f 100644 --- a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -15,12 +15,12 @@ - + all runtime; build; native; contentfiles; analyzers - - + + diff --git a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index e679181fb..0e58689d7 100644 --- a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 42e0bc9be..2be4be747 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -11,15 +11,15 @@ - - + + all runtime; build; native; contentfiles; analyzers - - + + - + diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs index 31e81f872..c0c7c8899 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs @@ -14,24 +14,9 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers public sealed class AssetChangedRuleTriggerDto : RuleTriggerDto { /// - /// Determines whether to handle the event when an asset is created. + /// Javascript condition when to trigger. /// - public bool SendCreate { get; set; } - - /// - /// Determines whether to handle the event when an asset is updated. - /// - public bool SendUpdate { get; set; } - - /// - /// Determines whether to handle the event when an asset is renamed. - /// - public bool SendRename { get; set; } - - /// - /// Determines whether to handle the event when an asset is deleted. - /// - public bool SendDelete { get; set; } + public string Condition { get; set; } public override RuleTrigger ToTrigger() { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs index 9570a5182..93c05c61d 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs @@ -17,38 +17,8 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers public Guid SchemaId { get; set; } /// - /// Determines whether to handle the event when a content is created. + /// Javascript condition when to trigger. /// - public bool SendCreate { get; set; } - - /// - /// Determines whether to handle the event when a content is updated. - /// - public bool SendUpdate { get; set; } - - /// - /// Determines whether to handle the event when a content is deleted. - /// - public bool SendDelete { get; set; } - - /// - /// Determines whether to handle the event when a content is published. - /// - public bool SendPublish { get; set; } - - /// - /// Determines whether to handle the event when a content is unpublished. - /// - public bool SendUnpublish { get; set; } - - /// - /// Determines whether to handle the event when a content is archived. - /// - public bool SendArchived { get; set; } - - /// - /// Determines whether to handle the event when a content is restored. - /// - public bool SendRestore { get; set; } + public string Condition { get; set; } } } diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 750f8bdde..40fab103a 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -54,7 +54,7 @@ - + @@ -74,14 +74,14 @@ - - - - - + + + + + - - + + diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 06226aa4d..64ca3d93f 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -13,6 +13,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Shared.Identity; @@ -38,7 +39,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "me") }); - sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator); + A.CallTo(() => urlGenerator.GenerateContentUIUrl(appId, schemaId, contentId)) + .Returns("content-url"); + + sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine()); } [Fact] @@ -69,91 +73,112 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Contains("MyEventName", result); } - [Fact] - public void Should_replace_app_information_from_event() + [Theory] + [InlineData("Name $APP_NAME has id $APP_ID")] + [InlineData("Script(`Name ${event.appId.name} has id ${event.appId.id}`)")] + public void Should_replace_app_information_from_event(string script) { var @event = new EnrichedContentEvent { AppId = appId }; - var result = sut.Format("Name $APP_NAME has id $APP_ID", @event); + var result = sut.Format(script, @event); Assert.Equal($"Name my-app has id {appId.Id}", result); } - [Fact] - public void Should_replace_schema_information_from_event() + [Theory] + [InlineData("Name $SCHEMA_NAME has id $SCHEMA_ID")] + [InlineData("Script(`Name ${event.schemaId.name} has id ${event.schemaId.id}`)")] + public void Should_replace_schema_information_from_event(string script) { var @event = new EnrichedContentEvent { SchemaId = schemaId }; - var result = sut.Format("Name $SCHEMA_NAME has id $SCHEMA_ID", @event); + var result = sut.Format(script, @event); Assert.Equal($"Name my-schema has id {schemaId.Id}", result); } - [Fact] - public void Should_replace_timestamp_information_from_event() + [Theory] + [InlineData("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME")] + [InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}, Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")] + public void Should_replace_timestamp_information_from_event(string script) { var now = DateTime.UtcNow; var envelope = new EnrichedContentEvent { Timestamp = Instant.FromDateTimeUtc(now) }; - var result = sut.Format("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope); + var result = sut.Format(script, envelope); Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result); } - [Fact] - public void Should_format_email_and_display_name_from_user() + [Theory] + [InlineData("From $USER_NAME ($USER_EMAIL)")] + [InlineData("Script(`From ${event.user.name} (${event.user.email})`)")] + public void Should_format_email_and_display_name_from_user(string script) { var @event = new EnrichedContentEvent { User = user, Actor = new RefToken(RefTokenType.Subject, "123") }; - var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event); + var result = sut.Format(script, @event); Assert.Equal("From me (me@email.com)", result); } - [Fact] - public void Should_return_undefined_if_user_is_not_found() + [Theory] + [InlineData("From $USER_NAME ($USER_EMAIL)")] + [InlineData("Script(`From ${event.user.name} (${event.user.email})`)")] + public void Should_return_null_if_user_is_not_found(string script) { var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Subject, "123") }; - var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event); + var result = sut.Format(script, @event); - Assert.Equal("From UNDEFINED (UNDEFINED)", result); + Assert.Equal("From null (null)", result); } - [Fact] - public void Should_format_email_and_display_name_from_client() + [Theory] + [InlineData("From $USER_NAME ($USER_EMAIL)")] + [InlineData("Script(`From ${event.user.name} (${event.user.email})`)", Skip = "Not Supported")] + public void Should_return_null_if_user_is_not_found_with_scripting(string script) { - var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") }; + var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Subject, "123") }; - var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event); + var result = sut.Format(script, @event); - Assert.Equal("From client:android (client:android)", result); + Assert.Equal("From null (null)", result); } - [Fact] - public void Should_replace_content_url_from_event() + [Theory] + [InlineData("Go to $CONTENT_URL")] + [InlineData("Script(`Go to ${contentUrl()}`)")] + public void Should_replace_content_url_from_event(string script) { - var url = "http://content"; - - A.CallTo(() => urlGenerator.GenerateContentUIUrl(appId, schemaId, contentId)) - .Returns(url); - var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId }; - var result = sut.Format("Go to $CONTENT_URL", @event); + var result = sut.Format(script, @event); - Assert.Equal($"Go to {url}", result); + Assert.Equal("Go to content-url", result); } - [Fact] - public void Should_format_content_url_when_not_found() + [Theory] + [InlineData("Go to $CONTENT_URL")] + [InlineData("Script(`Go to ${contentUrl()}`)")] + public void Should_format_content_url_when_not_found(string script) { - Assert.Equal("UNDEFINED", sut.Format("$CONTENT_URL", new EnrichedAssetEvent())); + Assert.Equal("Go to null", sut.Format(script, new EnrichedAssetEvent())); } - [Fact] - public void Should_return_undefined_when_field_not_found() + [Theory] + [InlineData("$CONTENT_ACTION")] + [InlineData("Script(`${event.type}`)")] + public void Should_format_content_actions_when_found(string script) + { + Assert.Equal("Created", sut.Format(script, new EnrichedContentEvent { Type = EnrichedContentEventType.Created })); + } + + [Theory] + [InlineData("$CONTENT_DATA.country.iv")] + [InlineData("Script(`${event.data.country.iv}`)")] + public void Should_return_null_when_field_not_found(string script) { var @event = new EnrichedContentEvent { @@ -164,13 +189,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.Format("$CONTENT_DATA.country.iv", @event); + var result = sut.Format(script, @event); - Assert.Equal("UNDEFINED", result); + Assert.Equal("null", result); } - [Fact] - public void Should_return_undefined_when_partition_not_found() + [Theory] + [InlineData("$CONTENT_DATA.city.de")] + [InlineData("Script(`${event.data.country.iv}`)")] + public void Should_return_null_when_partition_not_found(string script) { var @event = new EnrichedContentEvent { @@ -181,13 +208,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.Format("$CONTENT_DATA.city.de", @event); + var result = sut.Format(script, @event); - Assert.Equal("UNDEFINED", result); + Assert.Equal("null", result); } - [Fact] - public void Should_return_undefined_when_array_item_not_found() + [Theory] + [InlineData("$CONTENT_DATA.city.iv.10")] + [InlineData("Script(`${event.data.country.de[10]}`)")] + public void Should_return_null_when_array_item_not_found(string script) { var @event = new EnrichedContentEvent { @@ -198,13 +227,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", JsonValue.Array())) }; - var result = sut.Format("$CONTENT_DATA.city.de.10", @event); + var result = sut.Format(script, @event); - Assert.Equal("UNDEFINED", result); + Assert.Equal("null", result); } - [Fact] - public void Should_return_undefined_when_property_not_found() + [Theory] + [InlineData("$CONTENT_DATA.city.de.Name")] + [InlineData("Script(`${event.data.city.de.Location}`)")] + public void Should_return_null_when_property_not_found(string script) { var @event = new EnrichedContentEvent { @@ -215,13 +246,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) }; - var result = sut.Format("$CONTENT_DATA.city.de.Name", @event); + var result = sut.Format(script, @event); - Assert.Equal("UNDEFINED", result); + Assert.Equal("null", result); } - [Fact] - public void Should_return_plain_value_when_found() + [Theory] + [InlineData("$CONTENT_DATA.city.iv")] + [InlineData("Script(`${event.data.city.iv}`)")] + public void Should_return_plain_value_when_found(string script) { var @event = new EnrichedContentEvent { @@ -232,13 +265,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.Format("$CONTENT_DATA.city.iv", @event); + var result = sut.Format(script, @event); Assert.Equal("Berlin", result); } - [Fact] - public void Should_return_plain_value_when_found_from_update_event() + [Theory] + [InlineData("$CONTENT_DATA.city.iv.0")] + [InlineData("Script(`${event.data.city.iv[0]}`)")] + public void Should_return_plain_value_from_array_when_found(string script) { var @event = new EnrichedContentEvent { @@ -246,16 +281,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules new NamedContentData() .AddField("city", new ContentFieldData() - .AddValue("iv", "Berlin")) + .AddValue("iv", JsonValue.Array( + "Berlin"))) }; - var result = sut.Format("$CONTENT_DATA.city.iv", @event); + var result = sut.Format(script, @event); Assert.Equal("Berlin", result); } - [Fact] - public void Should_return_undefined_when_null() + [Theory] + [InlineData("$CONTENT_DATA.city.iv.name")] + [InlineData("Script(`${event.data.city.iv.name}`)")] + public void Should_return_plain_value_from_object_when_found(string script) { var @event = new EnrichedContentEvent { @@ -263,16 +301,18 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules new NamedContentData() .AddField("city", new ContentFieldData() - .AddValue("iv", JsonValue.Null)) + .AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) }; - var result = sut.Format("$CONTENT_DATA.city.iv", @event); + var result = sut.Format(script, @event); - Assert.Equal("UNDEFINED", result); + Assert.Equal("Berlin", result); } - [Fact] - public void Should_return_string_when_object() + [Theory] + [InlineData("$CONTENT_DATA.city.iv")] + [InlineData("Script(`${event.data.city.iv}`)", Skip = "Not Supported")] + public void Should_return_json_string_when_object(string script) { var @event = new EnrichedContentEvent { @@ -283,56 +323,27 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) }; - var result = sut.Format("$CONTENT_DATA.city.iv", @event); + var result = sut.Format(script, @event); Assert.Equal("{\"name\":\"Berlin\"}", result); } - [Fact] - public void Should_return_plain_value_from_array_when_found() + [Theory] + [InlineData("$CONTENT_ACTION")] + public void Should_null_when_content_action_not_found(string script) { - var @event = new EnrichedContentEvent - { - Data = - new NamedContentData() - .AddField("city", - new ContentFieldData() - .AddValue("iv", JsonValue.Array( - "Berlin"))) - }; - - var result = sut.Format("$CONTENT_DATA.city.iv.0", @event); - - Assert.Equal("Berlin", result); + Assert.Equal("null", sut.Format(script, new EnrichedAssetEvent())); } - [Fact] - public void Should_return_plain_value_from_object_when_found() + [Theory] + [InlineData("From $USER_NAME ($USER_EMAIL)")] + public void Should_format_email_and_display_name_from_client(string script) { - var @event = new EnrichedContentEvent - { - Data = - new NamedContentData() - .AddField("city", - new ContentFieldData() - .AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) - }; - - var result = sut.Format("$CONTENT_DATA.city.iv.name", @event); - - Assert.Equal("Berlin", result); - } + var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") }; - [Fact] - public void Should_format_content_actions_when_found() - { - Assert.Equal("created", sut.Format("$CONTENT_ACTION", new EnrichedContentEvent { Type = EnrichedContentEventType.Created })); - } + var result = sut.Format(script, @event); - [Fact] - public void Should_format_content_actions_when_not_found() - { - Assert.Equal("UNDEFINED", sut.Format("$CONTENT_ACTION", new EnrichedAssetEvent())); + Assert.Equal("From client:android (client:android)", result); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index c68aff69b..ed4d7b441 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index c3cd39700..20340d722 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index 6a0f7b16e..ca2daac0e 100644 --- a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 576a53a5c..02579d476 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/Squidex.Tests/Squidex.Tests.csproj b/tests/Squidex.Tests/Squidex.Tests.csproj index d1c85aa9d..43a04e1fe 100644 --- a/tests/Squidex.Tests/Squidex.Tests.csproj +++ b/tests/Squidex.Tests/Squidex.Tests.csproj @@ -11,11 +11,11 @@ - + - +