// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.Collections; using System.Globalization; using System.Reflection; using System.Security.Claims; using System.Text.RegularExpressions; using NodaTime; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Reflection.Internal; using Squidex.Shared.Users; using Squidex.Text; namespace Squidex.Domain.Apps.Core.Scripting; public sealed partial class ScriptingCompleter { private readonly IEnumerable descriptors; private static readonly FilterSchema DynamicData = new FilterSchema(FilterSchemaType.Object) { Fields = ReadonlyList.Create( new FilterField(new FilterSchema(FilterSchemaType.Object), "my-field"), new FilterField(FilterSchema.String, "my-field.iv")) }; public ScriptingCompleter(IEnumerable descriptors) { this.descriptors = descriptors; } public IReadOnlyList Trigger(string type) { Guard.NotNull(type); switch (type) { case "AssetChanged": return AssetTrigger(); case "Comment": return CommentTrigger(); case "ContentChanged": return ContentTrigger(DynamicData); case "SchemaChanged": return SchemaTrigger(); case "Usage": return UsageTrigger(); default: return new List(); } } public IReadOnlyList ContentScript(FilterSchema dataSchema) { Guard.NotNull(dataSchema); return new Process(descriptors, dataSchema.Flatten()).ContentScript(); } public IReadOnlyList ContentTrigger(FilterSchema dataSchema) { Guard.NotNull(dataSchema); return new Process(descriptors, dataSchema.Flatten()).ContentTrigger(); } public IReadOnlyList FieldRule(FilterSchema dataSchema) { Guard.NotNull(dataSchema); return new Process(descriptors, dataSchema.Flatten()).FieldRule(); } public IReadOnlyList PreviewUrl(FilterSchema dataSchema) { Guard.NotNull(dataSchema); return new Process(descriptors, dataSchema.Flatten()).PreviewUrl(); } public IReadOnlyList AssetScript() { return new Process(descriptors).AssetScript(); } public IReadOnlyList AssetTrigger() { return new Process(descriptors).AssetTrigger(); } public IReadOnlyList CommentTrigger() { return new Process(descriptors).CommentTrigger(); } public IReadOnlyList SchemaTrigger() { return new Process(descriptors).SchemaTrigger(); } public IReadOnlyList UsageTrigger() { return new Process(descriptors).UsageTrigger(); } private sealed partial class Process { private static readonly Regex RegexProperty = BuildPropertyRegex(); private readonly Stack prefixes = new Stack(); private readonly Dictionary result = new Dictionary(); private readonly IEnumerable descriptors; private readonly FilterSchema? dataSchema; public Process(IEnumerable descriptors, FilterSchema? dataSchema = null) { this.descriptors = descriptors; this.dataSchema = dataSchema; } public IReadOnlyList SchemaTrigger() { AddHelpers(ScriptScope.SchemaTrigger | ScriptScope.Async); AddObject("event", FieldDescriptions.Event, () => { AddType(typeof(EnrichedSchemaEvent)); }); return Build(); } public IReadOnlyList CommentTrigger() { AddHelpers(ScriptScope.CommentTrigger | ScriptScope.Async); AddObject("event", FieldDescriptions.Event, () => { AddType(typeof(EnrichedCommentEvent)); }); return Build(); } public IReadOnlyList UsageTrigger() { AddHelpers(ScriptScope.UsageTrigger | ScriptScope.Async); AddObject("event", FieldDescriptions.Event, () => { AddType(typeof(EnrichedUsageExceededEvent)); }); return Build(); } public IReadOnlyList ContentScript() { AddHelpers(ScriptScope.ContentScript | ScriptScope.Transform | ScriptScope.Async); AddObject("ctx", FieldDescriptions.Context, () => { AddType(typeof(ContentScriptVars)); }); return Build(); } public IReadOnlyList ContentTrigger() { AddHelpers(ScriptScope.ContentTrigger | ScriptScope.Async); AddObject("event", FieldDescriptions.Event, () => { AddType(typeof(EnrichedContentEvent)); }); return Build(); } public IReadOnlyList AssetTrigger() { AddHelpers(ScriptScope.AssetTrigger | ScriptScope.Async); AddObject("event", FieldDescriptions.Event, () => { AddType(typeof(EnrichedAssetEvent)); }); return Build(); } public IReadOnlyList AssetScript() { AddHelpers(ScriptScope.AssetScript | ScriptScope.Async); AddObject("ctx", FieldDescriptions.Event, () => { AddType(typeof(AssetScriptVars)); }); return Build(); } public IReadOnlyList PreviewUrl() { AddString("id", FieldDescriptions.EntityId); AddNumber("version", FieldDescriptions.EntityVersion); AddString("accessToken", FieldDescriptions.AccessToken); AddObject("data", FieldDescriptions.ContentData, () => { AddData(); }); return Build(); } public IReadOnlyList FieldRule() { AddObject("user", FieldDescriptions.User, () => { AddString("id", FieldDescriptions.UserId); AddString("email", FieldDescriptions.UserEmail); AddString("displayName", FieldDescriptions.UserDisplayName); AddString("role", FieldDescriptions.UserAppRole); }); AddObject("data", FieldDescriptions.ContentData, () => { AddData(); }); AddObject("itemData", FieldDescriptions.ItemData, () => { AddAny("my-field", FieldDescriptions.ItemDataField); }); return Build(); } private IReadOnlyList Build() { return result.Values.OrderBy(x => x.Path).ToList(); } private void AddHelpers(ScriptScope scope) { foreach (var descriptor in descriptors) { descriptor.Describe(Add, scope); } } private void AddType(Type type) { foreach (var (name, description, propertyTypeOrNullable) in GetFields(type)) { var propertyType = Nullable.GetUnderlyingType(propertyTypeOrNullable) ?? propertyTypeOrNullable; if (propertyType.IsEnum) { var allowedValues = Enum.GetValues(propertyType).OfType().Select(x => x.ToString()!).Order().ToArray(); Add(JsonType.String, name, description, allowedValues); } else if ( propertyType == typeof(string) || propertyType == typeof(DomainId) || propertyType == typeof(Instant) || propertyType == typeof(Status)) { AddString(name, description); } else if (propertyType == typeof(int) || propertyType == typeof(long)) { AddNumber(name, description); } else if (propertyType == typeof(bool)) { AddBoolean(name, description); } else if (typeof(MulticastDelegate).IsAssignableFrom(propertyType.BaseType)) { AddFunction(name, description); } else if (propertyType == typeof(AssetMetadata)) { AddObject(name, description, () => { AddString("my-name", FieldDescriptions.AssetMetadataValue); }); } else if (propertyType == typeof(NamedId)) { AddObject(name, description, () => { AddString("id", FieldDescriptions.NamedId); AddString("name", FieldDescriptions.NamedName); }); } else if (propertyType == typeof(RefToken)) { AddObject(name, description, () => { AddString("identifier", FieldDescriptions.ActorIdentifier); AddString("type", FieldDescriptions.ActorType); }); } else if (propertyType == typeof(IUser) || propertyType == typeof(ClaimsPrincipal)) { AddObject(name, description, () => { AddString("id", FieldDescriptions.UserId); AddString("email", FieldDescriptions.UserEmail); AddBoolean("isClient", FieldDescriptions.UserIsClient); AddBoolean("isUser", FieldDescriptions.UserIsUser); AddObject("claims", FieldDescriptions.UserClaims, () => { AddArray("name", FieldDescriptions.UsersClaimsValue); }); }); } else if (propertyType == typeof(ContentData) && dataSchema != null) { AddObject(name, description, () => { AddData(); }); } else if (GetFields(propertyType).Any()) { AddObject(name, description, () => { AddType(propertyType); }); } else if (propertyType.GetInterfaces().Contains(typeof(IEnumerable))) { AddArray(name, description); } } } private static IEnumerable<(string Name, string Description, Type Type)> GetFields(Type type) { foreach (var property in type.GetPublicProperties()) { var descriptionKey = property.GetCustomAttribute()?.Name; if (descriptionKey == null) { continue; } var description = FieldDescriptions.ResourceManager.GetString(descriptionKey, CultureInfo.InvariantCulture)!; yield return (property.Name.ToCamelCase(), description, property.PropertyType); } } private void AddData() { if (dataSchema?.Fields == null) { return; } foreach (var field in dataSchema.Fields) { JsonType type = ConvertType(field); Add(type, field.Path, field.Description, field.Schema.AllowedValues); } static JsonType ConvertType(FilterField field) { switch (field.Schema.Type) { case FilterSchemaType.Any: return JsonType.Any; case FilterSchemaType.Boolean: return JsonType.Boolean; case FilterSchemaType.DateTime: return JsonType.String; case FilterSchemaType.GeoObject: return JsonType.Object; case FilterSchemaType.Guid: return JsonType.String; case FilterSchemaType.Number: return JsonType.Number; case FilterSchemaType.Object: return JsonType.Object; case FilterSchemaType.ObjectArray: return JsonType.Array; case FilterSchemaType.String: return JsonType.String; case FilterSchemaType.StringArray: return JsonType.Array; default: return JsonType.String; } } } private void AddAny(string? name, string? description) { Add(JsonType.Any, name, description); } private void AddArray(string? name, string? description) { Add(JsonType.Array, name, description); } private void AddBoolean(string? name, string? description) { Add(JsonType.Boolean, name, description); } private void AddNumber(string? name, string? description) { Add(JsonType.Number, name, description); } private void AddFunction(string? name, string? description) { Add(JsonType.Function, name, description); } private void AddString(string? name, string? description) { Add(JsonType.String, name, description); } private void Add(JsonType type, string? name, string? description, string[]? allowedValues = null, string? decprecationReason = null) { var parts = name?.Split('.') ?? Array.Empty(); foreach (var part in parts) { PushPrefix(part); } if (prefixes.Count == 0) { return; } var path = string.Concat(prefixes.Reverse()); result[path] = new ScriptingValue(path, type) { AllowedValues = allowedValues, Description = description, DeprecationReason = decprecationReason }; for (int i = 0; i < parts.Length; i++) { prefixes.Pop(); } } private void AddObject(string name, string description, Action inner) { Add(JsonType.Object, name, description); var parts = name.Split('.'); foreach (var part in parts) { PushPrefix(part); } inner(); for (int i = 0; i < parts.Length; i++) { prefixes.Pop(); } } private void PushPrefix(string name) { if (prefixes.Count == 0) { prefixes.Push(name); } else if (RegexProperty.IsMatch(name)) { prefixes.Push($".{name}"); } else { prefixes.Push($"['{name}']"); } } [GeneratedRegex("^(?!\\d)[\\w$]+$", RegexOptions.Compiled)] private static partial Regex BuildPropertyRegex(); } }