diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 9f780a395..d4711801d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -28,7 +28,7 @@ - + diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index ec66d886d..c9d45c97e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -36,9 +36,11 @@ public sealed class ImageAssetMetadataSource : IAssetMetadataSource if (imageInfo != null) { - var isSwapped = imageInfo.Orientation > ImageOrientation.TopLeft; + var needsFix = + imageInfo.HasSensitiveMetadata || + imageInfo.Orientation > ImageOrientation.TopLeft; - if (command.File != null && isSwapped) + if (command.File != null && needsFix) { var tempFile = TempAssetFile.Create(command.File); @@ -46,7 +48,7 @@ public sealed class ImageAssetMetadataSource : IAssetMetadataSource { await using (var tempStream = tempFile.OpenWrite()) { - await assetThumbnailGenerator.FixOrientationAsync(uploadStream, mimeType, tempStream, ct); + await assetThumbnailGenerator.FixAsync(uploadStream, mimeType, tempStream, ct); } } @@ -60,7 +62,7 @@ public sealed class ImageAssetMetadataSource : IAssetMetadataSource command.File = tempFile; } - if (command.Type == AssetType.Unknown || isSwapped) + if (command.Type == AssetType.Unknown || needsFix) { command.Type = AssetType.Image; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs index af4e62466..739c735de 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs @@ -48,8 +48,6 @@ internal sealed class Builder public IInterfaceGraphType ComponentInterface { get; } = new ComponentInterfaceGraphType(); - public List Schemas { get; private set; } - public Builder(IAppEntity app, GraphQLOptions options) { partitionResolver = app.PartitionResolver(); @@ -66,9 +64,9 @@ internal sealed class Builder allSchemas.AddRange(SchemaInfo.Build(schemas, typeNames).Where(x => x.Fields.Count > 0)); // Only published normal schemas (not components are used for entities). - Schemas = allSchemas.Where(x => x.Schema.SchemaDef.IsPublished && x.Schema.SchemaDef.Type != SchemaType.Component).ToList(); + var normalSchemas = allSchemas.Where(x => x.Schema.SchemaDef.IsPublished && x.Schema.SchemaDef.Type != SchemaType.Component).ToList(); - foreach (var schemaInfo in Schemas) + foreach (var schemaInfo in normalSchemas) { var contentType = new ContentGraphType(schemaInfo); @@ -76,7 +74,7 @@ internal sealed class Builder contentResultTypes[schemaInfo] = new ContentResultGraphType(contentType, schemaInfo); } - foreach (var schemaInfo in Schemas) + foreach (var schemaInfo in normalSchemas) { var componentType = new ComponentGraphType(schemaInfo); @@ -85,7 +83,7 @@ internal sealed class Builder var newSchema = new GraphQLSchema { - Query = new ApplicationQueries(this, Schemas) + Query = new ApplicationQueries(this, normalSchemas) }; newSchema.RegisterType(ComponentInterface); @@ -94,9 +92,9 @@ internal sealed class Builder newSchema.Directives.Register(SharedTypes.CacheDirective); newSchema.Directives.Register(SharedTypes.OptimizeFieldQueriesDirective); - if (Schemas.Any()) + if (normalSchemas.Any()) { - var mutations = new ApplicationMutations(this, Schemas); + var mutations = new ApplicationMutations(this, normalSchemas); if (mutations.Fields.Count > 0) { @@ -111,7 +109,7 @@ internal sealed class Builder foreach (var (schemaInfo, contentType) in contentTypes) { - contentType.Initialize(this, schemaInfo, Schemas); + contentType.Initialize(this, schemaInfo, normalSchemas); } foreach (var (schemaInfo, componentType) in componentTypes) diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index db21de289..9bd81aeb4 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -24,12 +24,12 @@ - - - - - - + + + + + + diff --git a/backend/src/Squidex/Config/Web/WebExtensions.cs b/backend/src/Squidex/Config/Web/WebExtensions.cs index 4cd4c8f6a..5a788f8c2 100644 --- a/backend/src/Squidex/Config/Web/WebExtensions.cs +++ b/backend/src/Squidex/Config/Web/WebExtensions.cs @@ -32,7 +32,7 @@ public static class WebExtensions public static IApplicationBuilder UseSquidexLocalization(this IApplicationBuilder app) { - var supportedCultures = new[] { "en", "nl", "it", "zh", "pt","fr" }; + var supportedCultures = new[] { "en", "nl", "it", "zh", "pt", "fr" }; var localizationOptions = new RequestLocalizationOptions() .SetDefaultCulture(supportedCultures[0]) @@ -93,7 +93,7 @@ public static class WebExtensions httpContext.Response.Headers[HeaderNames.ContentType] = "application/json"; - return httpContext.Response.WriteAsync(json); + return httpContext.Response.WriteAsync(json, httpContext.RequestAborted); }); app.UseHealthChecks("/readiness", new HealthCheckOptions diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 034305341..befd10c0a 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -62,18 +62,18 @@ - - - - - - - - + + + + + + + + - - - + + + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs index 6481d8d74..66c736381 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs @@ -51,7 +51,7 @@ public class AppCommandMiddlewareTests : HandlerTestBase var file = new NoopAssetFile(); A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) - .Returns(new ImageInfo(100, 100, ImageOrientation.None, ImageFormat.PNG)); + .Returns(new ImageInfo(ImageFormat.PNG, 100, 100, ImageOrientation.None, false)); await HandleAsync(new UploadAppImage { File = file }, None.Value); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index 2e8e1ea20..c85cc15b7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -43,6 +43,9 @@ public class ImageAssetMetadataSourceTests : GivenContext { var command = new CreateAsset { File = file }; + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) + .Returns(Task.FromResult(null)); + await sut.EnhanceAsync(command, CancellationToken); Assert.Empty(command.Tags); @@ -54,7 +57,7 @@ public class ImageAssetMetadataSourceTests : GivenContext var command = new CreateAsset { File = file }; A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) - .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); + .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false)); await sut.EnhanceAsync(command, CancellationToken); @@ -62,7 +65,7 @@ public class ImageAssetMetadataSourceTests : GivenContext Assert.Equal(600, command.Metadata.GetPixelHeight()); Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A._, A._)) + A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A._, A._)) .MustNotHaveHappened(); } @@ -72,10 +75,31 @@ public class ImageAssetMetadataSourceTests : GivenContext var command = new CreateAsset { File = file }; A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) - .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); + .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false)); + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) + .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.BottomRight, false)).Once(); + + await sut.EnhanceAsync(command, CancellationToken); + + Assert.Equal(800, command.Metadata.GetPixelWidth()); + Assert.Equal(600, command.Metadata.GetPixelHeight()); + Assert.Equal(AssetType.Image, command.Type); + + A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A._, CancellationToken)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_fix_image_if_it_contains_sensitive_metadata() + { + var command = new CreateAsset { File = file }; + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) + .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false)); A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) - .Returns(new ImageInfo(600, 800, ImageOrientation.BottomRight, ImageFormat.PNG)).Once(); + .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, true)).Once(); await sut.EnhanceAsync(command, CancellationToken); @@ -83,7 +107,7 @@ public class ImageAssetMetadataSourceTests : GivenContext Assert.Equal(600, command.Metadata.GetPixelHeight()); Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A._, CancellationToken)) + A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A._, CancellationToken)) .MustHaveHappened(); } diff --git a/frontend/src/app/features/rules/pages/events/rule-event.component.html b/frontend/src/app/features/rules/pages/events/rule-event.component.html index 05c48cb3b..134fefa01 100644 --- a/frontend/src/app/features/rules/pages/events/rule-event.component.html +++ b/frontend/src/app/features/rules/pages/events/rule-event.component.html @@ -49,7 +49,7 @@ - + diff --git a/frontend/src/app/features/rules/shared/actions/formattable-input.component.html b/frontend/src/app/features/rules/shared/actions/formattable-input.component.html index bd75523bf..734103da1 100644 --- a/frontend/src/app/features/rules/shared/actions/formattable-input.component.html +++ b/frontend/src/app/features/rules/shared/actions/formattable-input.component.html @@ -1,6 +1,6 @@ - + @@ -17,5 +17,5 @@ - + \ No newline at end of file diff --git a/frontend/src/app/features/rules/shared/actions/formattable-input.component.scss b/frontend/src/app/features/rules/shared/actions/formattable-input.component.scss index 20f8b6287..502f4ea6b 100644 --- a/frontend/src/app/features/rules/shared/actions/formattable-input.component.scss +++ b/frontend/src/app/features/rules/shared/actions/formattable-input.component.scss @@ -30,6 +30,8 @@ sqx-code-editor { &.active { color: $color-black; + font-family: inherit; + font-weight: 500; } } } \ No newline at end of file diff --git a/frontend/src/app/features/rules/shared/actions/formattable-input.component.ts b/frontend/src/app/features/rules/shared/actions/formattable-input.component.ts index 0a22174d1..5eeb81a0e 100644 --- a/frontend/src/app/features/rules/shared/actions/formattable-input.component.ts +++ b/frontend/src/app/features/rules/shared/actions/formattable-input.component.ts @@ -6,17 +6,17 @@ */ import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, Input, ViewChild } from '@angular/core'; -import { ControlValueAccessor, DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { CodeEditorComponent, ScriptCompletions, Types } from '@app/framework'; +type TemplateMode = 'Text' | 'Script' | 'Liquid'; + +const TEMPLATE_MODES: ReadonlyArray = ['Text', 'Script', 'Liquid']; + export const SQX_FORMATTABLE_INPUT_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FormattableInputComponent), multi: true, }; -type TemplateMode = 'Text' | 'Script' | 'Liquid'; - -const MODES: ReadonlyArray = ['Text', 'Script', 'Liquid']; - @Component({ selector: 'sqx-formattable-input[type]', styleUrls: ['./formattable-input.component.scss'], @@ -37,91 +37,70 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie @Input() public completion: ScriptCompletions | undefined | null; - @ViewChild(DefaultValueAccessor) - public inputEditor!: DefaultValueAccessor; - @ViewChild(CodeEditorComponent) public codeEditor!: CodeEditorComponent; public disabled = false; - public modes = MODES; + public modes = TEMPLATE_MODES; public mode: TemplateMode = 'Text'; - public aceMode = 'ace/editor/text'; - - public get actualCompletion() { - return this.mode === 'Script' ? this.completion : null; - } - - public get valueAccessor(): ControlValueAccessor { - return this.codeEditor || this.inputEditor; - } + public editorMode = 'ace/mode/text'; + public editorCompletion?: ScriptCompletions | undefined | null; public ngAfterViewInit() { - this.valueAccessor.registerOnChange((value: any) => { + this.codeEditor.registerOnChange((value: any) => { this.value = value; - this.fnChanged(this.convertValue(value)); + this.fnChanged(getValueFromMode(value, this.mode)); }); - this.valueAccessor.registerOnTouched(() => { + this.codeEditor.registerOnTouched(() => { this.fnTouched(); }); - this.valueAccessor.writeValue(this.value); + this.codeEditor.writeValue(this.value); } public writeValue(obj: any) { - let mode: TemplateMode = 'Text'; + const { value, mode } = getModeFromValue(obj); - if (Types.isString(obj)) { - this.value = obj; - - if (obj.endsWith(')')) { - const lower = obj.toLowerCase(); - - if (lower.startsWith('liquid(')) { - this.value = obj.substring(7, obj.length - 1); - - mode = 'Liquid'; - } else if (lower.startsWith('script(')) { - this.value = obj.substring(7, obj.length - 1); - - mode = 'Script'; - } - } - } else { - this.value = undefined; - } + this.value = value; this.setMode(mode, false); - - this.valueAccessor?.writeValue(this.value); + this.codeEditor?.writeValue(value); } public setDisabledState(isDisabled: boolean) { - this.disabled = isDisabled; + this.setDisabled(isDisabled); + this.codeEditor?.setDisabledState?.(isDisabled); + } - this.valueAccessor?.setDisabledState?.(isDisabled); + private setDisabled(isDisabled: boolean) { + this.disabled = isDisabled; } public setMode(mode: TemplateMode, emit = true) { - if (this.mode !== mode) { - this.mode = mode; - - if (mode === 'Script') { - this.aceMode = 'ace/mode/javascript'; - } else if (mode === 'Liquid') { - this.aceMode = 'ace/mode/liquid'; - } else { - this.aceMode = 'ace/editor/text'; - } - - if (emit) { - this.fnChanged(this.convertValue(this.value)); - this.fnTouched(); - } + if (this.mode === mode) { + return; + } + + if (mode === 'Script') { + this.editorMode = 'ace/mode/javascript'; + this.editorCompletion = this.completion; + } else if (mode === 'Liquid') { + this.editorMode = 'ace/mode/liquid'; + this.editorCompletion = this.completion?.filter(x => x.type !== 'Function'); + } else { + this.editorMode = 'ace/mode/text'; + this.editorCompletion = this.completion?.filter(x => x.type !== 'Function'); + } + + this.mode = mode; + + if (emit) { + this.fnChanged(getValueFromMode(this.value, mode)); + this.fnTouched(); } } @@ -132,23 +111,45 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie public registerOnTouched(fn: any) { this.fnTouched = fn; } +} - private convertValue(value: string | undefined) { - if (!value) { - return value; - } +function getValueFromMode(value: string | undefined, mode: TemplateMode) { + if (!value) { + return value; + } - value = value.trim(); + value = value.trim(); - switch (this.mode) { - case 'Liquid': { - return `Liquid(${value})`; - } - case 'Script': { - return `Script(${value})`; - } + switch (mode) { + case 'Liquid': { + return `Liquid(${value})`; + } + case 'Script': { + `Script(${value})`; } + } - return value; + return value; +} + +function getModeFromValue(value: any): { value: string | undefined; mode: TemplateMode } { + if (!Types.isString(value) || !value) { + return { value, mode: 'Text' }; } + + if (value.endsWith(')')) { + const lower = value.toLowerCase(); + + if (lower.startsWith('liquid(')) { + value = value.substring(7, value.length - 1); + + return { value, mode: 'Liquid' }; + } else if (lower.startsWith('script(')) { + value = value.substring(7, value.length - 1); + + return { value, mode: 'Script' }; + } + } + + return { value, mode: 'Text' }; } diff --git a/frontend/src/app/framework/angular/forms/editors/code-editor.component.ts b/frontend/src/app/framework/angular/forms/editors/code-editor.component.ts index ded8fb0b2..604ae0fe4 100644 --- a/frontend/src/app/framework/angular/forms/editors/code-editor.component.ts +++ b/frontend/src/app/framework/angular/forms/editors/code-editor.component.ts @@ -27,7 +27,7 @@ export const SQX_CODE_EDITOR_CONTROL_VALUE_ACCESSOR: any = { ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CodeEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, FocusComponent { +export class CodeEditorComponent extends StatefulControlComponent<{}, any> implements AfterViewInit, FocusComponent { private aceEditor: any; private aceTools: any; private valueChanged = new Subject(); @@ -56,6 +56,9 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im @Input() public singleLine = false; + @Input() + public snippets = true; + @Input() public wordWrap = false; @@ -87,7 +90,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im this.setMode(); } - if (changes.height || changes.maxLines || changes.singleLine) { + if (changes.height || changes.maxLines || changes.singleLine || changes.snippets) { this.setOptions(); } @@ -96,7 +99,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im } } - public writeValue(obj: string) { + public writeValue(obj: any) { try { if (Types.isNull(obj) || Types.isUndefined(obj)) { this.value = ''; @@ -285,7 +288,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im autoScrollEditorIntoView: this.singleLine, enableBasicAutocompletion: !!this.aceTools, enableLiveAutocompletion: !!this.aceTools, - enableSnippets: !!this.aceTools && !this.singleLine, + enableSnippets: !!this.aceTools && !this.singleLine && this.snippets, highlightActiveLine: !this.singleLine, maxLines, minLines, diff --git a/tools/TestSuite/docker-compose.yml b/tools/TestSuite/docker-compose.yml index 70e865fbb..4f6f7e51f 100644 --- a/tools/TestSuite/docker-compose.yml +++ b/tools/TestSuite/docker-compose.yml @@ -85,7 +85,7 @@ services: - mongo resizer: - image: squidex/resizer:dev-40 + image: squidex/resizer:1.3.0 networks: - internal depends_on: