diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs index d96f8711b..9ba8ed1b9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs @@ -34,4 +34,54 @@ public static class JintExtensions return ids; } + + internal static ScriptExecutionContext ExtendAsync(this ScriptExecutionContext context, + IEnumerable extensions) + { + foreach (var extension in extensions) + { + extension.ExtendAsync(context); + } + + return context; + } + + internal static ScriptExecutionContext Extend(this ScriptExecutionContext context, + IEnumerable extensions) + { + foreach (var extension in extensions) + { + extension.Extend(context); + } + + return context; + } + + internal static ScriptExecutionContext Extend(this ScriptExecutionContext context, + ScriptVars vars, + ScriptOptions options) + { + var engine = context.Engine; + + context.CopyFrom(vars); + + if (options.AsContext) + { + var contextInstance = new WritableContext(engine, vars); + + engine.SetValue("ctx", contextInstance); + engine.SetValue("context", contextInstance); + } + else + { + foreach (var (key, item) in vars) + { + engine.SetValue(key, item.Value!); + } + } + + engine.SetValue("async", true); + + return context; + } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs index eac3d77f9..fcc9eeda7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs @@ -7,6 +7,7 @@ using Jint; using Squidex.Infrastructure.Tasks; +using System.Diagnostics; namespace Squidex.Domain.Apps.Core.Scripting; @@ -22,20 +23,12 @@ public abstract class ScriptExecutionContext : ScriptContext public abstract void Schedule(Func action); } -#pragma warning disable MA0048 // File name must match type name -public interface IScheduler -#pragma warning restore MA0048 // File name must match type name -{ - void Run(Action? action); - - void Run(Action? action, T argument); -} - public sealed class ScriptExecutionContext : ScriptExecutionContext, IScheduler { - private readonly TaskCompletionSource tcs = new TaskCompletionSource(); + private readonly TaskCompletionSource tcs = new TaskCompletionSource(); private readonly CancellationToken cancellationToken; - private int pendingTasks; + private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); + private int pendingTasks = 1; public bool IsCompleted { @@ -48,12 +41,9 @@ public sealed class ScriptExecutionContext : ScriptExecutionContext, ISchedul this.cancellationToken = cancellationToken; } - public Task CompleteAsync() + public Task CompleteAsync() { - if (pendingTasks <= 0) - { - tcs.TrySetResult(default); - } + TryComplete(default!); return tcs.Task.WithCancellation(cancellationToken); } @@ -74,95 +64,103 @@ public sealed class ScriptExecutionContext : ScriptExecutionContext, ISchedul { try { - Interlocked.Increment(ref pendingTasks); + TryStart(); await action(this, cancellationToken); - if (Interlocked.Decrement(ref pendingTasks) <= 0) - { - tcs.TrySetResult(default); - } + TryComplete(default!); } catch (Exception ex) { - tcs.TrySetException(ex); + TryFail(ex); } } ScheduleAsync().Forget(); } - public ScriptExecutionContext ExtendAsync(IEnumerable extensions) + void IScheduler.Run(Action? action) { - foreach (var extension in extensions) + if (IsCompleted || action == null) { - extension.ExtendAsync(this); + return; } - return this; - } + slimLock.EnterWriteLock(); + try + { + TryStart(); - public ScriptExecutionContext Extend(IEnumerable extensions) - { - foreach (var extension in extensions) + Engine.ResetConstraints(); + action(); + + TryComplete(default!); + } + catch (Exception ex) { - extension.Extend(this); + TryFail(ex); + } + finally + { + slimLock.ExitWriteLock(); } - - return this; } - public ScriptExecutionContext Extend(ScriptVars vars, ScriptOptions options) + void IScheduler.Run(Action? action, TArg argument) { - var engine = Engine; - - CopyFrom(vars); + if (IsCompleted || action == null) + { + return; + } - if (options.AsContext) + slimLock.EnterWriteLock(); + try { - var contextInstance = new WritableContext(engine, vars); + TryStart(); + + Engine.ResetConstraints(); + action(argument); - engine.SetValue("ctx", contextInstance); - engine.SetValue("context", contextInstance); + TryComplete(default!); } - else + catch (Exception ex) { - foreach (var (key, item) in vars) - { - engine.SetValue(key, item.Value!); - } + TryFail(ex); } + finally + { + slimLock.ExitWriteLock(); + } + } - engine.SetValue("async", true); - - return this; + private void TryFail(Exception exception) + { + tcs.TrySetException(exception); } - void IScheduler.Run(Action? action) + private void TryStart() { - lock (Engine) - { - if (IsCompleted || action == null) - { - return; - } + Interlocked.Increment(ref pendingTasks); - Engine.ResetConstraints(); - action(); - } + Debug.WriteLine(pendingTasks); } - void IScheduler.Run(Action? action, TArg argument) + private void TryComplete(T result) { - lock (Engine) + if (Interlocked.Decrement(ref pendingTasks) <= 0) { - if (IsCompleted || action == null) - { - return; - } - - Engine.ResetConstraints(); - action(argument); + tcs.TrySetResult(result); } + + Debug.WriteLine(pendingTasks); } } + +#pragma warning disable MA0048 // File name must match type name +public interface IScheduler +#pragma warning restore MA0048 // File name must match type name +{ + void Run(Action? action); + + void Run(Action? action, T argument); +} diff --git a/backend/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs index 44f7596f7..643533ff8 100644 --- a/backend/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionAttribute.cs @@ -73,7 +73,6 @@ public class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter if (!hasPermission) { context.Result = new StatusCodeResult(403); - return Task.CompletedTask; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index d9ae4da6e..3f4620133 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -73,7 +73,7 @@ public class JintScriptEngineTests : IClassFixture { context.Schedule(async (scheduler, ct) => { - await Task.Delay(5, ct); + await Task.Delay(1, ct); scheduler.Run(callback); }); })); @@ -621,12 +621,14 @@ public class JintScriptEngineTests : IClassFixture [Fact] public async Task Should_not_run_callbacks_in_parallel() { - var vars = new DataScriptVars + for (var i = 0; i < 10; i++) { - ["value"] = 13 - }; + var vars = new DataScriptVars + { + ["value"] = 13 + }; - const string script1 = @" + const string script1 = @" var x = ctx.value; for (var i = 0; i < 100; i++) { setTimeout(function () { @@ -636,8 +638,9 @@ public class JintScriptEngineTests : IClassFixture } "; - await sut.ExecuteAsync(vars, script1, new ScriptOptions { AsContext = true }); + await sut.ExecuteAsync(vars, script1, new ScriptOptions { AsContext = true }); - Assert.Equal(113.0, vars["shared"]); + Assert.Equal(113.0, vars["shared"]); + } } } diff --git a/frontend/src/app/framework/angular/pipes/highlight.pipe.ts b/frontend/src/app/framework/angular/pipes/highlight.pipe.ts index f5dd8f287..5f7a72511 100644 --- a/frontend/src/app/framework/angular/pipes/highlight.pipe.ts +++ b/frontend/src/app/framework/angular/pipes/highlight.pipe.ts @@ -6,7 +6,7 @@ */ import { Pipe, PipeTransform } from '@angular/core'; -import { Types } from '@app/framework/internal'; +import { escapeHTML, Types } from '@app/framework/internal'; @Pipe({ name: 'sqxHighlight', @@ -14,6 +14,8 @@ import { Types } from '@app/framework/internal'; }) export class HighlightPipe implements PipeTransform { public transform(text: string, highlight: string | RegExp | undefined | null): string { + text = escapeHTML(text); + if (!highlight) { return text; } @@ -22,6 +24,8 @@ export class HighlightPipe implements PipeTransform { highlight = new RegExp(highlight, 'i'); } - return text.replace(highlight, s => `${s}`); + const result = text.replace(highlight, s => `${s}`); + + return result; } } diff --git a/frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts b/frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts index c55b82e10..4c5f1688f 100644 --- a/frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts +++ b/frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts @@ -8,14 +8,14 @@ import { KeysPipe } from './keys.pipe'; describe('KeysPipe', () => { + const pipe = new KeysPipe(); + it('should return keys', () => { const value = { key1: 1, key2: 2, }; - const pipe = new KeysPipe(); - const actual = pipe.transform(value); const expected = ['key1', 'key2'];