Browse Source

Jurassic => Jint.

pull/104/head
Sebastian Stehle 9 years ago
parent
commit
0c873c9ba8
  1. 14
      src/Squidex.Domain.Apps.Core/Contents/ContentData.cs
  2. 6
      src/Squidex.Domain.Apps.Core/Contents/ContentFieldData.cs
  3. 5
      src/Squidex.Domain.Apps.Core/Contents/IdContentData.cs
  4. 3
      src/Squidex.Domain.Apps.Core/Contents/NamedContentData.cs
  5. 132
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentDataObject.cs
  6. 67
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentDataProperty.cs
  7. 142
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentFieldObject.cs
  8. 58
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentFieldProperty.cs
  9. 146
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/JsonMapper.cs
  10. 92
      src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs
  11. 50
      src/Squidex.Domain.Apps.Core/Scripting/JintUser.cs
  12. 3
      src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs
  13. 50
      src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs
  14. 1
      src/Squidex.Domain.Apps.Core/Squidex.Domain.Apps.Core.csproj
  15. 2
      src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs
  16. 2
      src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs
  17. 43
      src/Squidex.Infrastructure/CollectionExtensions.cs
  18. 6
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  19. 216
      tests/Squidex.Domain.Apps.Core.Tests/Scripting/ContentDataObjectTests.cs
  20. 59
      tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs
  21. 73
      tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintUserTests.cs
  22. 80
      tests/Squidex.Domain.Apps.Core.Tests/Scripting/ScriptUserTests.cs

14
src/Squidex.Domain.Apps.Core/Contents/ContentData.cs

@ -9,9 +9,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
// ReSharper disable InvertIf
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{
var resultValue = new ContentFieldData();
foreach (var partitionValue in fieldValue.Value.Where(x => IsNotNull(x.Value)))
foreach (var partitionValue in fieldValue.Value.Where(x => !x.Value.IsNull()))
{
resultValue[partitionValue.Key] = partitionValue.Value;
}
@ -120,16 +120,6 @@ namespace Squidex.Domain.Apps.Core.Contents
return this.DictionaryHashCode();
}
protected static bool IsNull(JToken value)
{
return value == null || value.Type == JTokenType.Null;
}
protected static bool IsNotNull(JToken value)
{
return value != null && value.Type != JTokenType.Null;
}
public abstract T GetKey(Field field);
}
}

6
src/Squidex.Domain.Apps.Core/Contents/ContentFieldData.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class ContentFieldData : Dictionary<string, JToken>, IEquatable<ContentFieldData>
{
private static readonly JTokenEqualityComparer JTokenEqualityComparer = new JTokenEqualityComparer();
public ContentFieldData()
: base(StringComparer.OrdinalIgnoreCase)
{
@ -43,12 +45,12 @@ namespace Squidex.Domain.Apps.Core.Contents
public bool Equals(ContentFieldData other)
{
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other));
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other, EqualityComparer<string>.Default, JTokenEqualityComparer));
}
public override int GetHashCode()
{
return this.DictionaryHashCode();
return this.DictionaryHashCode(EqualityComparer<string>.Default, JTokenEqualityComparer);
}
}
}

5
src/Squidex.Domain.Apps.Core/Contents/IdContentData.cs

@ -13,6 +13,7 @@ using System.Text;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
// ReSharper disable InvertIf
@ -65,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Contents
continue;
}
foreach (var partitionValue in fieldData.Where(x => IsNotNull(x.Value)).ToList())
foreach (var partitionValue in fieldData.Where(x => !x.Value.IsNull()).ToList())
{
var newValue = referenceField.RemoveDeletedReferences(partitionValue.Value, deletedReferencedIds);
@ -96,7 +97,7 @@ namespace Squidex.Domain.Apps.Core.Contents
foreach (var partitionValue in fieldValue.Value)
{
if (IsNull(partitionValue.Value))
if (partitionValue.Value.IsNull())
{
encodedValue[partitionValue.Key] = null;
}

3
src/Squidex.Domain.Apps.Core/Contents/NamedContentData.cs

@ -13,6 +13,7 @@ using System.Text;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
// ReSharper disable InvertIf
@ -65,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Contents
foreach (var partitionValue in fieldValue.Value)
{
if (IsNull(partitionValue.Value))
if (partitionValue.Value.IsNull())
{
encodedValue[partitionValue.Key] = null;
}

132
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentDataObject.cs

@ -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)));
}
}
}
}
}

67
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentDataProperty.cs

@ -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);
}
}
}
}

142
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentFieldObject.cs

@ -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));
}
}
}
}
}

58
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentFieldProperty.cs

@ -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;
}
}
}

146
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/JsonMapper.cs

@ -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));
}
}
}

92
src/Squidex.Domain.Apps.Core/Scripting/JurassicScriptEngine.cs → src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs

@ -1,5 +1,5 @@
// ==========================================================================
// JurassicScriptEngine.cs
// JintScriptEngine.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -7,28 +7,20 @@
// ==========================================================================
using System;
using Jurassic;
using Jurassic.Library;
using Newtonsoft.Json;
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
// ReSharper disable ConvertToLambdaExpression
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class JurassicScriptEngine : IScriptEngine
public sealed class JintScriptEngine : IScriptEngine
{
private readonly JsonSerializerSettings serializerSettings;
public JurassicScriptEngine(JsonSerializerSettings serializerSettings)
{
Guard.NotNull(serializerSettings, nameof(serializerSettings));
this.serializerSettings = serializerSettings;
}
public void Execute(ScriptContext context, string script, string operationName)
{
Guard.NotNull(context, nameof(context));
@ -57,15 +49,13 @@ namespace Squidex.Domain.Apps.Core.Scripting
EnableDisallow(engine);
EnableReject(engine, operationName);
engine.SetGlobalFunction("replace", new Action<object>(data =>
engine.SetValue("replace", new Action(() =>
{
try
{
result = JsonConvert.DeserializeObject<NamedContentData>(JSONObject.Stringify(engine, data));
}
catch
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
result = new NamedContentData();
data.TryUpdate(out result);
}
}));
@ -87,9 +77,14 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
var engine = CreateScriptEngine(context);
engine.SetGlobalFunction("replace", new Action<object>(data =>
engine.SetValue("replace", new Action(() =>
{
result = JsonConvert.DeserializeObject<NamedContentData>(JSONObject.Stringify(engine, data));
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
data.TryUpdate(out result);
}
}));
}
catch (Exception)
@ -101,46 +96,67 @@ namespace Squidex.Domain.Apps.Core.Scripting
return result;
}
private static void Execute(ScriptEngine engine, string script, string operationName)
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 error.", new ValidationError(ex.Message));
}
catch (JavaScriptException ex)
{
throw new ValidationException($"Failed to {operationName} with javascript error.", new ValidationError(ex.Message));
}
}
private ScriptEngine CreateScriptEngine(ScriptContext context)
private static Engine CreateScriptEngine(ScriptContext context)
{
var engine = new ScriptEngine { ForceStrictMode = true };
var engine = new Engine(options => options.TimeoutInterval(TimeSpan.FromMilliseconds(100)).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.SetGlobalValue("ctx", JSONObject.Parse(engine, JsonConvert.SerializeObject(context, serializerSettings)));
engine.SetValue("ctx", contextInstance);
return engine;
}
private static void EnableReject(ScriptEngine engine, string operationName)
private static void EnableDisallow(Engine engine)
{
Guard.NotNullOrEmpty(operationName, nameof(operationName));
engine.SetGlobalFunction("reject", new Action<string>(message =>
engine.SetValue("disallow", new Action<string>(message =>
{
var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null;
var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed";
throw new ValidationException($"Script rejected to to {operationName}.", errors);
throw new DomainForbiddenException(exMessage);
}));
}
private static void EnableDisallow(ScriptEngine engine)
private static void EnableReject(Engine engine, string operationName)
{
engine.SetGlobalFunction("disallow", new Action<string>(message =>
Guard.NotNullOrEmpty(operationName, nameof(operationName));
engine.SetValue("reject", new Action<string>(message =>
{
var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed";
var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null;
throw new DomainForbiddenException(exMessage);
throw new ValidationException($"Script rejected to to {operationName}.", errors);
}));
}
}

50
src/Squidex.Domain.Apps.Core/Scripting/JintUser.cs

@ -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);
}
}
}

3
src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs

@ -7,13 +7,14 @@
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptContext
{
public ScriptUser User { get; set; }
public ClaimsPrincipal User { get; set; }
public Guid ContentId { get; set; }

50
src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs

@ -1,50 +0,0 @@
// ==========================================================================
// ScriptUser.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptUser
{
public bool IsClient { get; set; }
public string Id { get; set; }
public string Email { get; set; }
public Dictionary<string, string[]> Claims { get; set; }
public static ScriptUser Create(ClaimsPrincipal principal)
{
Guard.NotNull(principal, nameof(principal));
var subjectId = principal.OpenIdSubject();
var user = new ScriptUser { IsClient = string.IsNullOrWhiteSpace(subjectId), Email = principal.OpenIdEmail() };
if (!user.IsClient)
{
user.Id = subjectId;
}
else
{
user.Id = principal.OpenIdClientId();
}
user.Claims = principal.Claims.GroupBy(x => x.Type).ToDictionary(x => x.Key, x => x.Select(y => y.Value).ToArray());
return user;
}
}
}

1
src/Squidex.Domain.Apps.Core/Squidex.Domain.Apps.Core.csproj

@ -10,6 +10,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="2.11.23" />
<PackageReference Include="Jurassic" Version="4.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />

2
src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs

@ -196,7 +196,7 @@ namespace Squidex.Domain.Apps.Write.Contents
private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, NamedContentData data = null)
{
return new ScriptContext { ContentId = content.Id, Data = data, OldData = content.Data, User = ScriptUser.Create(command.User) };
return new ScriptContext { ContentId = content.Id, Data = data, OldData = content.Data, User = command.User };
}
private async Task<(ISchemaEntity SchemaEntity, IAppEntity AppEntity)> ResolveSchemaAndAppAsync(SchemaCommand command)

2
src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Users
{
public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory<IUser, IRole>
{
public UserClaimsPrincipalFactoryWithEmail(UserManager<IUser> userManager, RoleManager<IRole> roleManager, IOptions<IdentityOptions> optionsAccessor)
public UserClaimsPrincipalFactoryWithEmail(UserManager<IUser> userManager, RoleManager<IRole> roleManager, IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{
}

43
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -60,31 +60,36 @@ namespace Squidex.Infrastructure
public static int DictionaryHashCode<TKey, TValue>(this IDictionary<TKey, TValue> dictionary)
{
return DictionaryHashCode(dictionary, EqualityComparer<TValue>.Default);
return DictionaryHashCode(dictionary, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default);
}
public static int DictionaryHashCode<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, IEqualityComparer<TValue> comparer)
public static int DictionaryHashCode<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
Guard.NotNull(comparer, nameof(comparer));
var hashCode = 17;
foreach (var kvp in dictionary.OrderBy(x => x.Key))
{
hashCode = hashCode * 23 + kvp.Key.GetHashCode();
hashCode = hashCode * 23 + keyComparer.GetHashCode(kvp.Key);
if (kvp.Value != null)
{
hashCode = hashCode * 23 + comparer.GetHashCode(kvp.Value);
hashCode = hashCode * 23 + valueComparer.GetHashCode(kvp.Value);
}
}
return hashCode;
}
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IDictionary<TKey, TValue> other)
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue> other)
{
return other != null && dictionary.Count == other.Count && !dictionary.Except(other).Any();
return EqualsDictionary(dictionary, other, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default);
}
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue> other, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
var comparer = new KeyValuePairComparer<TKey, TValue>(keyComparer, valueComparer);
return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any();
}
public static TValue GetOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
@ -136,5 +141,27 @@ namespace Squidex.Infrastructure
action(item);
}
}
public sealed class KeyValuePairComparer<TKey, TValue> : IEqualityComparer<KeyValuePair<TKey, TValue>>
{
private readonly IEqualityComparer<TKey> keyComparer;
private readonly IEqualityComparer<TValue> valueComparer;
public KeyValuePairComparer(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
this.keyComparer = keyComparer;
this.valueComparer = valueComparer;
}
public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y)
{
return keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value);
}
public int GetHashCode(KeyValuePair<TKey, TValue> obj)
{
return keyComparer.GetHashCode(obj.Key) ^ valueComparer.GetHashCode(obj.Value);
}
}
}
}

6
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -105,7 +105,6 @@ namespace Squidex.Controllers.ContentApi
await Task.WhenAll(taskForItems, taskForCount);
var scriptUser = ScriptUser.Create(User);
var scriptText = schemaEntity.ScriptQuery;
var hasScript = !string.IsNullOrWhiteSpace(scriptText);
@ -123,7 +122,7 @@ namespace Squidex.Controllers.ContentApi
if (hasScript && !isFrontendClient)
{
data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = item.Id, User = scriptUser }, scriptText);
data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = item.Id, User = User }, scriptText);
}
itemModel.Data = data;
@ -166,14 +165,13 @@ namespace Squidex.Controllers.ContentApi
if (!isFrontendClient)
{
var scriptUser = ScriptUser.Create(User);
var scriptText = schemaEntity.ScriptQuery;
var hasScript = !string.IsNullOrWhiteSpace(scriptText);
if (hasScript)
{
data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = entity.Id, User = scriptUser }, scriptText);
data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = entity.Id, User = User }, scriptText);
}
}

216
tests/Squidex.Domain.Apps.Core.Tests/Scripting/ContentDataObjectTests.cs

@ -0,0 +1,216 @@
// ==========================================================================
// ContentDataObjectTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Jint;
using Jint.Runtime;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Xunit;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ContentDataObjectTests
{
[Fact]
public void Should_update_content_when_setting_field()
{
var original = new NamedContentData();
var expected =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", 1.0));
var result = ExecuteScript(original, @"data.number = { iv: 1 }");
Assert.Equal(expected, result);
}
[Fact]
public void Should_update_content_when_deleting_field()
{
var original =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", 1.0));
var expected = new NamedContentData();
var result = ExecuteScript(original, @"delete data.number");
Assert.Equal(expected, result);
}
[Fact]
public void Should_update_content_when_setting_field_value_with_string()
{
var original =
new NamedContentData()
.AddField("string",
new ContentFieldData()
.AddValue("iv", "1"));
var expected =
new NamedContentData()
.AddField("string",
new ContentFieldData()
.AddValue("iv", "1new"));
var result = ExecuteScript(original, @"data.string.iv = data.string.iv + 'new'");
Assert.Equal(expected, result);
}
[Fact]
public void Should_update_content_when_setting_field_value_with_number()
{
var original =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", 1.0));
var expected =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", 3.0));
var result = ExecuteScript(original, @"data.number.iv = data.number.iv + 2");
Assert.Equal(expected, result);
}
[Fact]
public void Should_update_content_when_setting_field_value_with_array()
{
var original =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", new JArray(1.0, 2.0)));
var expected =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", new JArray(1.0, 4.0, 5.0)));
var result = ExecuteScript(original, @"data.number.iv = [data.number.iv[0], data.number.iv[1] + 2, 5]");
Assert.Equal(expected, result);
}
[Fact]
public void Should_update_content_when_setting_field_value_with_object()
{
var original =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", new JObject(new JProperty("lat", 1.0))));
var expected =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", new JObject(new JProperty("lat", 1.0), new JProperty("lon", 4.0))));
var result = ExecuteScript(original, @"data.number.iv = { lat: data.number.iv.lat, lon: data.number.iv.lat + 3 }");
Assert.Equal(expected, result);
}
[Fact]
public void Should_update_content_when_setting_field_value_with_boolean()
{
var original =
new NamedContentData()
.AddField("boolean",
new ContentFieldData()
.AddValue("iv", false));
var expected =
new NamedContentData()
.AddField("boolean",
new ContentFieldData()
.AddValue("iv", true));
var result = ExecuteScript(original, @"data.boolean.iv = !data.boolean.iv");
Assert.Equal(expected, result);
}
[Fact]
public void Should_update_content_when_deleting_field_value()
{
var original =
new NamedContentData()
.AddField("string",
new ContentFieldData()
.AddValue("iv", "hello"));
var expected =
new NamedContentData()
.AddField("string",
new ContentFieldData());
var result = ExecuteScript(original, @"delete data.string.iv");
Assert.Equal(expected, result);
}
[Fact]
public void Should_throw_exceptions_when_changing_objects()
{
var original =
new NamedContentData()
.AddField("obj",
new ContentFieldData()
.AddValue("iv", new JObject(new JProperty("readonly", 1))));
Assert.Throws<JavaScriptException>(() => ExecuteScript(original, "data.obj.iv.invalid = 1"));
Assert.Throws<JavaScriptException>(() => ExecuteScript(original, "data.obj.iv.readonly = 2"));
}
[Fact]
public void Should_not_throw_exceptions_when_changing_arrays()
{
var original =
new NamedContentData()
.AddField("obj",
new ContentFieldData()
.AddValue("iv", new JArray()));
ExecuteScript(original, "data.obj.iv[0] = 1");
}
[Fact]
public void Should_null_propagate_unknown_fields()
{
ExecuteScript(new NamedContentData(), @"data.string.iv = 'hello'");
}
private static NamedContentData ExecuteScript(NamedContentData original, string script)
{
var engine = new Engine(o => o.Strict());
var value = new ContentDataObject(engine, original);
engine.SetValue("data", value);
engine.Execute(script);
value.TryUpdate(out var result);
return result;
}
}
}

59
tests/Squidex.Domain.Apps.Core.Tests/Scripting/JurassicScriptEngineTests.cs → tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs

@ -1,27 +1,20 @@
// ==========================================================================
// JurassicScriptEngineTests.cs
// JintScriptEngineTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Core.Scripting
{
public class JurassicScriptEngineTests
public class JintScriptEngineTests
{
private readonly JurassicScriptEngine scriptEngine =
new JurassicScriptEngine(
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
private readonly JintScriptEngine scriptEngine = new JintScriptEngine();
[Fact]
public void Should_throw_validation_exception_when_calling_reject()
@ -86,44 +79,6 @@ namespace Squidex.Domain.Apps.Core.Scripting
Assert.Same(content, result);
}
[Fact]
public void Should_return_original_content_when_replacing_with_invalid_content_in_transform()
{
var content =
new NamedContentData()
.AddField("number0",
new ContentFieldData()
.AddValue("iv", 1))
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 1));
var context = new ScriptContext { Data = content };
var result = scriptEngine.Transform(context, @"replace({ test: 1 });");
Assert.Equal(content, result);
}
[Fact]
public void Should_return_empty_content_when_replacing_with_invalid_content_in_execute_and_transform()
{
var content =
new NamedContentData()
.AddField("number0",
new ContentFieldData()
.AddValue("iv", 1))
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 1));
var context = new ScriptContext { Data = content };
var result = scriptEngine.ExecuteAndTransform(context, @"replace({ test: 1 });", "update");
Assert.Equal(new NamedContentData(), result);
}
[Fact]
public void Should_transform_content_and_return()
{
@ -131,18 +86,18 @@ namespace Squidex.Domain.Apps.Core.Scripting
new NamedContentData()
.AddField("number0",
new ContentFieldData()
.AddValue("iv", 1))
.AddValue("iv", 1.0))
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 1));
.AddValue("iv", 1.0));
var expected =
new NamedContentData()
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 2))
.AddValue("iv", 2.0))
.AddField("number2",
new ContentFieldData()
.AddValue("iv", 10));
.AddValue("iv", 10.0));
var context = new ScriptContext { Data = content };

73
tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintUserTests.cs

@ -0,0 +1,73 @@
// ==========================================================================
// JintUserTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Security.Claims;
using Jint;
using Squidex.Infrastructure.Security;
using Xunit;
namespace Squidex.Domain.Apps.Core.Scripting
{
public class JintUserTests
{
[Fact]
public void Should_set_user_id_from_client_id()
{
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "1"));
Assert.Equal("1", GetValue(identity, "user.id"));
Assert.Equal(true, GetValue(identity, "user.isClient"));
}
[Fact]
public void Should_set_user_id_from_subject_id()
{
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(OpenIdClaims.Subject, "2"));
Assert.Equal("2", GetValue(identity, "user.id"));
Assert.Equal(false, GetValue(identity, "user.isClient"));
}
[Fact]
public void Should_set_email_from_claim()
{
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(OpenIdClaims.Email, "hello@squidex.io"));
Assert.Equal("hello@squidex.io", GetValue(identity, "user.email"));
}
[Fact]
public void Should_set_claims()
{
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim("claim1", "1a"));
identity.AddClaim(new Claim("claim1", "1b"));
identity.AddClaim(new Claim("claim2", "2a"));
identity.AddClaim(new Claim("claim2", "2b"));
Assert.Equal(new[] { "1a", "1b" }, GetValue(identity, "user.claims.claim1"));
Assert.Equal(new[] { "2a", "2b" }, GetValue(identity, "user.claims.claim2"));
}
private static object GetValue(ClaimsIdentity identity, string script)
{
var engine = new Engine();
engine.SetValue("user", new JintUser(engine, new ClaimsPrincipal(new[] { identity })));
return engine.Execute(script).GetCompletionValue().ToObject();
}
}
}

80
tests/Squidex.Domain.Apps.Core.Tests/Scripting/ScriptUserTests.cs

@ -1,80 +0,0 @@
// ==========================================================================
// ScriptUserTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Security.Claims;
using FluentAssertions;
using Squidex.Infrastructure.Security;
using Xunit;
namespace Squidex.Domain.Apps.Core.Scripting
{
public class ScriptUserTests
{
[Fact]
public void Should_create_script_user_from_user_principal()
{
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(OpenIdClaims.Subject, "1"));
identity.AddClaim(new Claim(OpenIdClaims.Email, "hello@squidex.io"));
identity.AddClaim(new Claim("claim1", "1a"));
identity.AddClaim(new Claim("claim1", "1b"));
identity.AddClaim(new Claim("claim2", "2a"));
identity.AddClaim(new Claim("claim2", "2b"));
var principal = new ClaimsPrincipal(new[] { identity });
var scriptUser = ScriptUser.Create(principal);
scriptUser.ShouldBeEquivalentTo(
new ScriptUser
{
Email = "hello@squidex.io",
Id = "1",
IsClient = false,
Claims = new Dictionary<string, string[]>
{
{ "sub", new [] { "1" } },
{ "claim1", new[] { "1a", "1b" } },
{ "claim2", new[] { "2a", "2b" } },
{ "email", new [] { "hello@squidex.io" } }
}
});
}
[Fact]
public void Should_create_script_user_from_client_principal()
{
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "1"));
identity.AddClaim(new Claim("claim1", "1a"));
identity.AddClaim(new Claim("claim1", "1b"));
identity.AddClaim(new Claim("claim2", "2a"));
identity.AddClaim(new Claim("claim2", "2b"));
var principal = new ClaimsPrincipal(new[] { identity });
var scriptUser = ScriptUser.Create(principal);
scriptUser.ShouldBeEquivalentTo(
new ScriptUser
{
Id = "1",
IsClient = true,
Claims = new Dictionary<string, string[]>
{
{ "client_id", new [] { "1" } } ,
{ "claim1", new[] { "1a", "1b" } },
{ "claim2", new[] { "2a", "2b" } }
}
});
}
}
}
Loading…
Cancel
Save