mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
113 changed files with 2552 additions and 175 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@ |
|||
p41pr8YRXXiqrW5DrLpCxbsyTZwZ9ONcdFjYaHorvCuvPeMiBgl+WOFKkzLENEnJdh22tkh7FKjfSMW1S1jtZg== |
|||
@ -0,0 +1,16 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> |
|||
<metadata> |
|||
<id>Jurassic</id> |
|||
<version>4.0.0</version> |
|||
<authors>Paul Bartrum</authors> |
|||
<owners>Paul Bartrum</owners> |
|||
<requireLicenseAcceptance>false</requireLicenseAcceptance> |
|||
<description>A .NET library to parse and execute JavaScript code.</description> |
|||
<dependencies> |
|||
<group targetFramework=".NETStandard2.0"> |
|||
<dependency id="System.Reflection.Emit.Lightweight" version="4.3.0" exclude="Build,Analyzers" /> |
|||
</group> |
|||
</dependencies> |
|||
</metadata> |
|||
</package> |
|||
@ -0,0 +1,132 @@ |
|||
// ==========================================================================
|
|||
// ContentDataObject.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Jint; |
|||
using Jint.Native; |
|||
using Jint.Native.Object; |
|||
using Jint.Runtime.Descriptors; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper |
|||
{ |
|||
public sealed class ContentDataObject : ObjectInstance |
|||
{ |
|||
private readonly NamedContentData contentData; |
|||
private HashSet<string> fieldsToDelete; |
|||
private Dictionary<string, ContentDataProperty> fieldProperties; |
|||
private bool isChanged; |
|||
|
|||
public ContentDataObject(Engine engine, NamedContentData contentData) |
|||
: base(engine) |
|||
{ |
|||
Extensible = true; |
|||
|
|||
this.contentData = contentData; |
|||
} |
|||
|
|||
public void MarkChanged() |
|||
{ |
|||
isChanged = true; |
|||
} |
|||
|
|||
public bool TryUpdate(out NamedContentData result) |
|||
{ |
|||
result = contentData; |
|||
|
|||
if (isChanged) |
|||
{ |
|||
if (fieldsToDelete != null) |
|||
{ |
|||
foreach (var field in fieldsToDelete) |
|||
{ |
|||
contentData.Remove(field); |
|||
} |
|||
} |
|||
|
|||
if (fieldProperties != null) |
|||
{ |
|||
foreach (var kvp in fieldProperties) |
|||
{ |
|||
if (kvp.Value.ContentField.TryUpdate(out var fieldData)) |
|||
{ |
|||
contentData[kvp.Key] = fieldData; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return isChanged; |
|||
} |
|||
|
|||
public override void RemoveOwnProperty(string propertyName) |
|||
{ |
|||
if (fieldsToDelete == null) |
|||
{ |
|||
fieldsToDelete = new HashSet<string>(); |
|||
} |
|||
|
|||
fieldsToDelete.Add(propertyName); |
|||
fieldProperties?.Remove(propertyName); |
|||
|
|||
MarkChanged(); |
|||
} |
|||
|
|||
public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
if (!fieldProperties.ContainsKey(propertyName)) |
|||
{ |
|||
fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public override void Put(string propertyName, JsValue value, bool throwOnError) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this)).Value = value; |
|||
} |
|||
|
|||
public override PropertyDescriptor GetOwnProperty(string propertyName) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
return fieldProperties.GetOrDefault(propertyName) ?? new PropertyDescriptor(new ObjectInstance(Engine) { Extensible = true }, true, false, true); |
|||
} |
|||
|
|||
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties() |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
foreach (var property in fieldProperties) |
|||
{ |
|||
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value); |
|||
} |
|||
} |
|||
|
|||
private void EnsurePropertiesInitialized() |
|||
{ |
|||
if (fieldProperties == null) |
|||
{ |
|||
fieldProperties = new Dictionary<string, ContentDataProperty>(contentData.Count); |
|||
|
|||
foreach (var kvp in contentData) |
|||
{ |
|||
fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
// ==========================================================================
|
|||
// ContentFieldProperty.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Jint.Native; |
|||
using Jint.Runtime; |
|||
using Jint.Runtime.Descriptors; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper |
|||
{ |
|||
public sealed class ContentDataProperty : PropertyDescriptor |
|||
{ |
|||
private readonly ContentDataObject contentData; |
|||
private ContentFieldObject contentField; |
|||
private JsValue value; |
|||
|
|||
public override JsValue Value |
|||
{ |
|||
get { return value; } |
|||
set |
|||
{ |
|||
if (!Equals(this.value, value)) |
|||
{ |
|||
if (value == null || !value.IsObject()) |
|||
{ |
|||
throw new JavaScriptException("Can only assign object to content data."); |
|||
} |
|||
|
|||
var obj = value.AsObject(); |
|||
|
|||
contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); |
|||
|
|||
foreach (var kvp in obj.GetOwnProperties()) |
|||
{ |
|||
contentField.Put(kvp.Key, kvp.Value.Value, true); |
|||
} |
|||
|
|||
this.value = new JsValue(contentField); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public ContentFieldObject ContentField |
|||
{ |
|||
get { return contentField; } |
|||
} |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
// ==========================================================================
|
|||
// ContentFieldObject.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Jint.Native.Object; |
|||
using Jint.Runtime.Descriptors; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper |
|||
{ |
|||
public sealed class ContentFieldObject : ObjectInstance |
|||
{ |
|||
private readonly ContentDataObject contentData; |
|||
private readonly ContentFieldData fieldData; |
|||
private HashSet<string> valuesToDelete; |
|||
private Dictionary<string, ContentFieldProperty> valueProperties; |
|||
private bool isChanged; |
|||
|
|||
public bool IsChanged |
|||
{ |
|||
get { return isChanged; } |
|||
} |
|||
|
|||
public ContentFieldData FieldData |
|||
{ |
|||
get { return fieldData; } |
|||
} |
|||
|
|||
public ContentFieldObject(ContentDataObject contentData, ContentFieldData fieldData, bool isNew) |
|||
: base(contentData.Engine) |
|||
{ |
|||
Extensible = true; |
|||
|
|||
this.contentData = contentData; |
|||
this.fieldData = fieldData; |
|||
|
|||
if (isNew) |
|||
{ |
|||
MarkChanged(); |
|||
} |
|||
} |
|||
|
|||
public void MarkChanged() |
|||
{ |
|||
isChanged = true; |
|||
|
|||
contentData.MarkChanged(); |
|||
} |
|||
|
|||
public bool TryUpdate(out ContentFieldData result) |
|||
{ |
|||
result = fieldData; |
|||
|
|||
if (isChanged) |
|||
{ |
|||
if (valuesToDelete != null) |
|||
{ |
|||
foreach (var field in valuesToDelete) |
|||
{ |
|||
fieldData.Remove(field); |
|||
} |
|||
} |
|||
|
|||
if (valueProperties != null) |
|||
{ |
|||
foreach (var kvp in valueProperties) |
|||
{ |
|||
if (kvp.Value.IsChanged) |
|||
{ |
|||
fieldData[kvp.Key] = kvp.Value.ContentValue; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return isChanged; |
|||
} |
|||
|
|||
public override void RemoveOwnProperty(string propertyName) |
|||
{ |
|||
if (valuesToDelete == null) |
|||
{ |
|||
valuesToDelete = new HashSet<string>(); |
|||
} |
|||
|
|||
valuesToDelete.Add(propertyName); |
|||
valueProperties?.Remove(propertyName); |
|||
|
|||
MarkChanged(); |
|||
} |
|||
|
|||
public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
if (!valueProperties.ContainsKey(propertyName)) |
|||
{ |
|||
valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public override PropertyDescriptor GetOwnProperty(string propertyName) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined; |
|||
} |
|||
|
|||
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties() |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
foreach (var property in valueProperties) |
|||
{ |
|||
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value); |
|||
} |
|||
} |
|||
|
|||
private void EnsurePropertiesInitialized() |
|||
{ |
|||
if (valueProperties == null) |
|||
{ |
|||
valueProperties = new Dictionary<string, ContentFieldProperty>(FieldData.Count); |
|||
|
|||
foreach (var kvp in FieldData) |
|||
{ |
|||
valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
// ==========================================================================
|
|||
// ContentFieldProperty.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Jint.Native; |
|||
using Jint.Runtime.Descriptors; |
|||
using Newtonsoft.Json.Linq; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper |
|||
{ |
|||
public sealed class ContentFieldProperty : PropertyDescriptor |
|||
{ |
|||
private readonly ContentFieldObject contentField; |
|||
private JToken contentValue; |
|||
private JsValue value; |
|||
private bool isChanged; |
|||
|
|||
public override JsValue Value |
|||
{ |
|||
get { return value ?? (value = JsonMapper.Map(contentValue, contentField.Engine)); } |
|||
set |
|||
{ |
|||
if (!Equals(this.value, value)) |
|||
{ |
|||
this.value = value; |
|||
|
|||
contentValue = null; |
|||
contentField.MarkChanged(); |
|||
|
|||
isChanged = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public JToken ContentValue |
|||
{ |
|||
get { return contentValue ?? (contentValue = JsonMapper.Map(value)); } |
|||
} |
|||
|
|||
public bool IsChanged |
|||
{ |
|||
get { return isChanged; } |
|||
} |
|||
|
|||
public ContentFieldProperty(ContentFieldObject contentField, JToken contentValue = null) |
|||
: base(null, true, true, true) |
|||
{ |
|||
this.contentField = contentField; |
|||
this.contentValue = contentValue; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
// ==========================================================================
|
|||
// JsonMapper.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Jint; |
|||
using Jint.Native; |
|||
using Jint.Native.Object; |
|||
using Newtonsoft.Json.Linq; |
|||
|
|||
// ReSharper disable SwitchStatementMissingSomeCases
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper |
|||
{ |
|||
public static class JsonMapper |
|||
{ |
|||
public static JsValue Map(JToken value, Engine engine) |
|||
{ |
|||
if (value == null) |
|||
{ |
|||
return JsValue.Null; |
|||
} |
|||
|
|||
switch (value.Type) |
|||
{ |
|||
case JTokenType.Date: |
|||
case JTokenType.Guid: |
|||
case JTokenType.String: |
|||
case JTokenType.Uri: |
|||
case JTokenType.TimeSpan: |
|||
return new JsValue((string)value); |
|||
case JTokenType.Null: |
|||
return JsValue.Null; |
|||
case JTokenType.Undefined: |
|||
return JsValue.Undefined; |
|||
case JTokenType.Integer: |
|||
return new JsValue((long)value); |
|||
case JTokenType.Float: |
|||
return new JsValue((double)value); |
|||
case JTokenType.Boolean: |
|||
return new JsValue((bool)value); |
|||
case JTokenType.Object: |
|||
{ |
|||
var obj = (JObject)value; |
|||
|
|||
var target = new ObjectInstance(engine); |
|||
|
|||
foreach (var property in obj) |
|||
{ |
|||
target.FastAddProperty(property.Key, Map(property.Value, engine), false, true, true); |
|||
} |
|||
|
|||
return target; |
|||
} |
|||
case JTokenType.Array: |
|||
{ |
|||
var arr = (JArray)value; |
|||
|
|||
var target = new JsValue[arr.Count]; |
|||
|
|||
for (var i = 0; i < arr.Count; i++) |
|||
{ |
|||
target[i] = Map(arr[i], engine); |
|||
} |
|||
|
|||
return engine.Array.Construct(target); |
|||
} |
|||
} |
|||
|
|||
throw new ArgumentException("Invalid json type", nameof(value)); |
|||
} |
|||
|
|||
public static JToken Map(JsValue value) |
|||
{ |
|||
if (value == null || value.IsNull()) |
|||
{ |
|||
return JValue.CreateNull(); |
|||
} |
|||
|
|||
if (value.IsUndefined()) |
|||
{ |
|||
return JValue.CreateUndefined(); |
|||
} |
|||
|
|||
if (value.IsString()) |
|||
{ |
|||
return new JValue(value.AsString()); |
|||
} |
|||
|
|||
if (value.IsBoolean()) |
|||
{ |
|||
return new JValue(value.AsBoolean()); |
|||
} |
|||
|
|||
if (value.IsNumber()) |
|||
{ |
|||
return new JValue(value.AsNumber()); |
|||
} |
|||
|
|||
if (value.IsDate()) |
|||
{ |
|||
return new JValue(value.AsDate().ToDateTime()); |
|||
} |
|||
|
|||
if (value.IsRegExp()) |
|||
{ |
|||
return JValue.CreateString(value.AsRegExp().Value?.ToString()); |
|||
} |
|||
|
|||
if (value.IsArray()) |
|||
{ |
|||
var arr = value.AsArray(); |
|||
|
|||
var target = new JArray(); |
|||
|
|||
for (var i = 0; i < arr.GetLength(); i++) |
|||
{ |
|||
target.Add(Map(arr.Get(i.ToString()))); |
|||
} |
|||
|
|||
return target; |
|||
} |
|||
|
|||
if (value.IsObject()) |
|||
{ |
|||
var obj = value.AsObject(); |
|||
|
|||
var target = new JObject(); |
|||
|
|||
foreach (var kvp in obj.GetOwnProperties()) |
|||
{ |
|||
target[kvp.Key] = Map(kvp.Value.Value); |
|||
} |
|||
|
|||
return target; |
|||
} |
|||
|
|||
throw new ArgumentException("Invalid json type", nameof(value)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// ==========================================================================
|
|||
// IScriptEngine.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting |
|||
{ |
|||
public interface IScriptEngine |
|||
{ |
|||
void Execute(ScriptContext context, string script, string operationName); |
|||
|
|||
NamedContentData ExecuteAndTransform(ScriptContext context, string script, string operationName); |
|||
|
|||
NamedContentData Transform(ScriptContext context, string script); |
|||
} |
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
// ==========================================================================
|
|||
// JintScriptEngine.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Jint; |
|||
using Jint.Native.Object; |
|||
using Jint.Parser; |
|||
using Jint.Runtime; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; |
|||
using Squidex.Infrastructure; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting |
|||
{ |
|||
public sealed class JintScriptEngine : IScriptEngine |
|||
{ |
|||
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); |
|||
|
|||
public void Execute(ScriptContext context, string script, string operationName) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
if (!string.IsNullOrWhiteSpace(script)) |
|||
{ |
|||
var engine = CreateScriptEngine(context); |
|||
|
|||
EnableDisallow(engine); |
|||
EnableReject(engine, operationName); |
|||
|
|||
Execute(engine, script, operationName); |
|||
} |
|||
} |
|||
|
|||
public NamedContentData ExecuteAndTransform(ScriptContext context, string script, string operationName) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
var result = context.Data; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(script)) |
|||
{ |
|||
var engine = CreateScriptEngine(context); |
|||
|
|||
EnableDisallow(engine); |
|||
EnableReject(engine, operationName); |
|||
|
|||
engine.SetValue("replace", new Action(() => |
|||
{ |
|||
var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); |
|||
|
|||
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) |
|||
{ |
|||
data.TryUpdate(out result); |
|||
} |
|||
})); |
|||
|
|||
Execute(engine, script, operationName); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public NamedContentData Transform(ScriptContext context, string script) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
var result = context.Data; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(script)) |
|||
{ |
|||
try |
|||
{ |
|||
var engine = CreateScriptEngine(context); |
|||
|
|||
engine.SetValue("replace", new Action(() => |
|||
{ |
|||
var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); |
|||
|
|||
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) |
|||
{ |
|||
data.TryUpdate(out result); |
|||
} |
|||
})); |
|||
|
|||
engine.Execute(script); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
result = context.Data; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static void Execute(Engine engine, string script, string operationName) |
|||
{ |
|||
try |
|||
{ |
|||
engine.Execute(script); |
|||
} |
|||
catch (ParserException ex) |
|||
{ |
|||
throw new ValidationException($"Failed to {operationName} with javascript syntaxs error.", new ValidationError(ex.Message)); |
|||
} |
|||
catch (JavaScriptException ex) |
|||
{ |
|||
throw new ValidationException($"Failed to {operationName} with javascript error.", new ValidationError(ex.Message)); |
|||
} |
|||
} |
|||
|
|||
private Engine CreateScriptEngine(ScriptContext context) |
|||
{ |
|||
var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict()); |
|||
|
|||
var contextInstance = new ObjectInstance(engine); |
|||
|
|||
if (context.Data != null) |
|||
{ |
|||
contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); |
|||
} |
|||
|
|||
if (context.OldData != null) |
|||
{ |
|||
contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.OldData), true, true, true); |
|||
} |
|||
|
|||
if (context.User != null) |
|||
{ |
|||
contextInstance.FastAddProperty("user", new JintUser(engine, context.User), false, true, false); |
|||
} |
|||
|
|||
engine.SetValue("ctx", contextInstance); |
|||
|
|||
return engine; |
|||
} |
|||
|
|||
private static void EnableDisallow(Engine engine) |
|||
{ |
|||
engine.SetValue("disallow", new Action<string>(message => |
|||
{ |
|||
var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; |
|||
|
|||
throw new DomainForbiddenException(exMessage); |
|||
})); |
|||
} |
|||
|
|||
private static void EnableReject(Engine engine, string operationName) |
|||
{ |
|||
Guard.NotNullOrEmpty(operationName, nameof(operationName)); |
|||
|
|||
engine.SetValue("reject", new Action<string>(message => |
|||
{ |
|||
var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; |
|||
|
|||
throw new ValidationException($"Script rejected to {operationName}.", errors); |
|||
})); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
// ==========================================================================
|
|||
// JintUser.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using Jint; |
|||
using Jint.Native; |
|||
using Jint.Native.Object; |
|||
using Squidex.Infrastructure.Security; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting |
|||
{ |
|||
public sealed class JintUser : ObjectInstance |
|||
{ |
|||
public JintUser(Engine engine, ClaimsPrincipal principal) |
|||
: base(engine) |
|||
{ |
|||
var subjectId = principal.OpenIdSubject(); |
|||
|
|||
var isClient = string.IsNullOrWhiteSpace(subjectId); |
|||
|
|||
if (!isClient) |
|||
{ |
|||
FastAddProperty("id", subjectId, false, true, false); |
|||
FastAddProperty("isClient", false, false, true, false); |
|||
} |
|||
else |
|||
{ |
|||
FastAddProperty("id", principal.OpenIdClientId(), false, true, false); |
|||
FastAddProperty("isClient", true, false, true, false); |
|||
} |
|||
|
|||
FastAddProperty("email", principal.OpenIdEmail(), false, true, false); |
|||
|
|||
var claimsInstance = new ObjectInstance(engine); |
|||
|
|||
foreach (var group in principal.Claims.GroupBy(x => x.Type)) |
|||
{ |
|||
claimsInstance.FastAddProperty(group.Key, engine.Array.Construct(group.Select(x => new JsValue(x.Value)).ToArray()), false, true, false); |
|||
} |
|||
|
|||
FastAddProperty("claims", claimsInstance, false, true, false); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// ScriptContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Security.Claims; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting |
|||
{ |
|||
public sealed class ScriptContext |
|||
{ |
|||
public ClaimsPrincipal User { get; set; } |
|||
|
|||
public Guid ContentId { get; set; } |
|||
|
|||
public NamedContentData Data { get; set; } |
|||
|
|||
public NamedContentData OldData { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// ScriptsConfigured.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Schemas |
|||
{ |
|||
[TypeName("ScriptsConfiguredEvent")] |
|||
public sealed class ScriptsConfigured : SchemaEvent |
|||
{ |
|||
public string ScriptQuery { get; set; } |
|||
|
|||
public string ScriptCreate { get; set; } |
|||
|
|||
public string ScriptUpdate { get; set; } |
|||
|
|||
public string ScriptDelete { get; set; } |
|||
|
|||
public string ScriptPublish { get; set; } |
|||
|
|||
public string ScriptUnpublish { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// ==========================================================================
|
|||
// ContentChangedResult.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure.CQRS.Commands; |
|||
|
|||
namespace Squidex.Domain.Apps.Write.Contents |
|||
{ |
|||
public sealed class ContentDataChangedResult : EntitySavedResult |
|||
{ |
|||
public NamedContentData Data { get; } |
|||
|
|||
public ContentDataChangedResult(NamedContentData data, long version) |
|||
: base(version) |
|||
{ |
|||
Data = data; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// ConfigureScripts.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Write.Schemas.Commands |
|||
{ |
|||
public sealed class ConfigureScripts : SchemaAggregateCommand |
|||
{ |
|||
public string ScriptQuery { get; set; } |
|||
|
|||
public string ScriptCreate { get; set; } |
|||
|
|||
public string ScriptUpdate { get; set; } |
|||
|
|||
public string ScriptDelete { get; set; } |
|||
|
|||
public string ScriptPublish { get; set; } |
|||
|
|||
public string ScriptUnpublish { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
// ==========================================================================
|
|||
// UserClaimsPrincipalFactoryWithEmail.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Infrastructure.Security; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Users |
|||
{ |
|||
public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory<IUser, IRole> |
|||
{ |
|||
public UserClaimsPrincipalFactoryWithEmail(UserManager<IUser> userManager, RoleManager<IRole> roleManager, IOptions<IdentityOptions> optionsAccessor) |
|||
: base(userManager, roleManager, optionsAccessor) |
|||
{ |
|||
} |
|||
|
|||
public override async Task<ClaimsPrincipal> CreateAsync(IUser user) |
|||
{ |
|||
var principal = await base.CreateAsync(user); |
|||
|
|||
principal.Identities.First().AddClaim(new Claim(OpenIdClaims.Email, await UserManager.GetEmailAsync(user))); |
|||
|
|||
return principal; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// DomainForbiddenException.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
|
|||
namespace Squidex.Infrastructure |
|||
{ |
|||
public class DomainForbiddenException : DomainException |
|||
{ |
|||
public DomainForbiddenException(string message) |
|||
: base(message) |
|||
{ |
|||
} |
|||
|
|||
public DomainForbiddenException(string message, Exception inner) |
|||
: base(message, inner) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
// ==========================================================================
|
|||
// ConfigureScriptsDto.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Controllers.Api.Schemas.Models |
|||
{ |
|||
public sealed class ConfigureScriptsDto |
|||
{ |
|||
/// <summary>
|
|||
/// The script that is executed for each query when querying contents.
|
|||
/// </summary>
|
|||
public string ScriptQuery { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The script that is executed when creating a content.
|
|||
/// </summary>
|
|||
public string ScriptCreate { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The script that is executed when updating a content.
|
|||
/// </summary>
|
|||
public string ScriptUpdate { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The script that is executed when deleting a content.
|
|||
/// </summary>
|
|||
public string ScriptDelete { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The script that is executed when publishing a content.
|
|||
/// </summary>
|
|||
public string ScriptPublish { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The script that is executed when unpublishing a content.
|
|||
/// </summary>
|
|||
public string ScriptUnpublish { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<form [formGroup]="editForm" (ngSubmit)="saveSchema()"> |
|||
<div class="modal-header"> |
|||
<h4 class="modal-title">Scripts</h4> |
|||
|
|||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="cancel()"> |
|||
<span aria-hidden="true">×</span> |
|||
</button> |
|||
</div> |
|||
|
|||
<div class="modal-body"> |
|||
<ul class="nav nav-tabs"> |
|||
<li class="nav-item" *ngFor="let script of scripts"> |
|||
<a class="nav-link" [class.active]="selectedField === 'script' + script" (click)="selectField('script' + script)">{{script}}</a> |
|||
</li> |
|||
</ul> |
|||
|
|||
<div class="form-group"> |
|||
<div *ngFor="let script of scripts"> |
|||
<div *ngIf="selectedField === 'script' + script"> |
|||
<sqx-jscript-editor name="script" [formControlName]="'script' + script"></sqx-jscript-editor> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="modal-footer"> |
|||
<div class="clearfix"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()" [disabled]="editFormSubmitted">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary">Save</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
.nav-link { |
|||
cursor: default; |
|||
} |
|||
|
|||
.nav-tabs { |
|||
border: 0; |
|||
} |
|||
|
|||
.clearfix { |
|||
width: 100%; |
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
|||
import { FormBuilder } from '@angular/forms'; |
|||
|
|||
import { |
|||
ComponentBase, |
|||
DialogService, |
|||
SchemaDetailsDto, |
|||
SchemasService, |
|||
UpdateSchemaScriptsDto |
|||
} from 'shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-schema-scripts-form', |
|||
styleUrls: ['./schema-scripts-form.component.scss'], |
|||
templateUrl: './schema-scripts-form.component.html' |
|||
}) |
|||
export class SchemaScriptsFormComponent extends ComponentBase implements OnInit { |
|||
@Output() |
|||
public saved = new EventEmitter<UpdateSchemaScriptsDto>(); |
|||
|
|||
@Output() |
|||
public cancelled = new EventEmitter(); |
|||
|
|||
@Input() |
|||
public schema: SchemaDetailsDto; |
|||
|
|||
@Input() |
|||
public appName: string; |
|||
|
|||
public selectedField = 'scriptQuery'; |
|||
|
|||
public scripts = [ |
|||
'Query', |
|||
'Create', |
|||
'Update', |
|||
'Delete', |
|||
'Publish', |
|||
'Unpublish' |
|||
]; |
|||
|
|||
public editFormSubmitted = false; |
|||
public editForm = |
|||
this.formBuilder.group({ |
|||
scriptQuery: '', |
|||
scriptCreate: '', |
|||
scriptUpdate: '', |
|||
scriptDelete: '', |
|||
scriptPublish: '', |
|||
scriptUnpublish: '' |
|||
}); |
|||
|
|||
constructor(dialogs: DialogService, |
|||
private readonly schemas: SchemasService, |
|||
private readonly formBuilder: FormBuilder |
|||
) { |
|||
super(dialogs); |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.editForm.patchValue(this.schema); |
|||
} |
|||
|
|||
public cancel() { |
|||
this.emitCancelled(); |
|||
this.resetEditForm(); |
|||
} |
|||
|
|||
public saveSchema() { |
|||
this.editFormSubmitted = true; |
|||
|
|||
if (this.editForm.valid) { |
|||
this.editForm.disable(); |
|||
|
|||
const requestDto = |
|||
new UpdateSchemaScriptsDto( |
|||
this.editForm.controls['scriptQuery'].value, |
|||
this.editForm.controls['scriptCreate'].value, |
|||
this.editForm.controls['scriptUpdate'].value, |
|||
this.editForm.controls['scriptDelete'].value, |
|||
this.editForm.controls['scriptPublish'].value, |
|||
this.editForm.controls['scriptUnpublish'].value); |
|||
|
|||
this.schemas.putSchemaScripts(this.appName, this.schema.name, requestDto, this.schema.version) |
|||
.subscribe(dto => { |
|||
this.emitSaved(requestDto); |
|||
this.resetEditForm(); |
|||
}, error => { |
|||
this.notifyError(error); |
|||
this.enableEditForm(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private emitCancelled() { |
|||
this.cancelled.emit(); |
|||
} |
|||
|
|||
private emitSaved(requestDto: UpdateSchemaScriptsDto) { |
|||
this.saved.emit(requestDto); |
|||
} |
|||
|
|||
public selectField(field: string) { |
|||
this.selectedField = field; |
|||
} |
|||
|
|||
private enableEditForm() { |
|||
this.editForm.enable(); |
|||
this.editFormSubmitted = false; |
|||
} |
|||
|
|||
private resetEditForm() { |
|||
this.editForm.reset(); |
|||
this.editForm.enable(); |
|||
this.editFormSubmitted = false; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
<div class="editor" #editor></div> |
|||
@ -0,0 +1,8 @@ |
|||
@import '_mixins'; |
|||
@import '_vars'; |
|||
|
|||
.editor { |
|||
background: $color-dark-foreground; |
|||
border: 1px solid $color-input; |
|||
height: 20rem; |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { AfterViewInit, Component, forwardRef, ElementRef, ViewChild } from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
import { Subject } from 'rxjs'; |
|||
|
|||
import { ResourceLoaderService } from './../services/resource-loader.service'; |
|||
|
|||
declare var ace: any; |
|||
|
|||
const NOOP = () => { /* NOOP */ }; |
|||
|
|||
export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { |
|||
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JscriptEditorComponent), multi: true |
|||
}; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-jscript-editor', |
|||
styleUrls: ['./jscript-editor.component.scss'], |
|||
templateUrl: './jscript-editor.component.html', |
|||
providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR] |
|||
}) |
|||
export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit { |
|||
private changeCallback: (value: any) => void = NOOP; |
|||
private touchedCallback: () => void = NOOP; |
|||
private valueChanged = new Subject(); |
|||
private aceEditor: any; |
|||
private oldValue: string; |
|||
private isDisabled = false; |
|||
|
|||
@ViewChild('editor') |
|||
public editor: ElementRef; |
|||
|
|||
constructor( |
|||
private readonly resourceLoader: ResourceLoaderService |
|||
) { |
|||
} |
|||
|
|||
public writeValue(value: any) { |
|||
this.oldValue = value; |
|||
|
|||
if (this.aceEditor) { |
|||
this.setValue(value); |
|||
} |
|||
} |
|||
|
|||
public setDisabledState(isDisabled: boolean): void { |
|||
this.isDisabled = isDisabled; |
|||
|
|||
if (this.aceEditor) { |
|||
this.aceEditor.setReadOnly(isDisabled); |
|||
} |
|||
} |
|||
|
|||
public registerOnChange(fn: any) { |
|||
this.changeCallback = fn; |
|||
} |
|||
|
|||
public registerOnTouched(fn: any) { |
|||
this.touchedCallback = fn; |
|||
} |
|||
|
|||
public ngAfterViewInit() { |
|||
this.valueChanged.debounceTime(500) |
|||
.subscribe(() => { |
|||
this.changeValue(); |
|||
}); |
|||
|
|||
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js').then(() => { |
|||
this.aceEditor = ace.edit(this.editor.nativeElement); |
|||
|
|||
this.aceEditor.getSession().setMode('ace/mode/javascript'); |
|||
this.aceEditor.setReadOnly(this.isDisabled); |
|||
this.aceEditor.setFontSize(14); |
|||
|
|||
this.setValue(this.oldValue); |
|||
|
|||
this.aceEditor.on('blur', () => { |
|||
this.changeValue(); |
|||
this.touchedCallback(); |
|||
}); |
|||
|
|||
this.aceEditor.on('change', () => { |
|||
this.valueChanged.next(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
private changeValue() { |
|||
const newValue = this.aceEditor.getValue(); |
|||
|
|||
if (this.oldValue !== newValue) { |
|||
this.changeCallback(newValue); |
|||
} |
|||
|
|||
this.oldValue = newValue; |
|||
} |
|||
|
|||
private setValue(value: string) { |
|||
this.aceEditor.setValue(value || ''); |
|||
this.aceEditor.clearSelection(); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue