diff --git a/README.md b/README.md index c58433c7c..8a732ed0b 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Please create issues to report bugs, suggest new functionalities, ask questions ## Cloud Version -Although Squidex is free we are also working on a Saas version on [https://cloud.squidex.io](https://cloud.squidex.io) (More information coming soon). We have also have plans to sell a premium version with first class support and some exlusive features. But don't be afraid, our first priority is to deliver a state of the art, stable, fast and free content management hub to make the life for developers a little bit easier. +Although Squidex is free it is also available as a Saas version on [https://cloud.squidex.io](https://cloud.squidex.io). ## License diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs index 0d05fc571..15c3b936b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Core.EnrichContent public IJsonValue Visit(IField field) { - return JsonValue.Object(); + return JsonValue.Null; } public IJsonValue Visit(IField field) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 85de85c82..bd68f23b6 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -30,12 +30,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { internal class MongoContentCollection : MongoRepositoryBase { - protected IJsonSerializer Serializer { get; } + private readonly IAppProvider appProvider; + private readonly IJsonSerializer serializer; - public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer) + public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) : base(database) { - Serializer = serializer; + this.appProvider = appProvider; + + this.serializer = serializer; } protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) @@ -43,9 +46,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return collection.Indexes.CreateManyAsync(new[] { new CreateIndexModel(Index - .Ascending(x => x.IndexedSchemaId).Ascending(x => x.Id).Ascending(x => x.Status)), + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Status) + .Ascending(x => x.Id)), + new CreateIndexModel(Index + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Status) + .Ascending(x => x.Id)), new CreateIndexModel(Index - .Ascending(x => x.ScheduledAt).Ascending(x => x.IsDeleted)), + .Ascending(x => x.ScheduledAt) + .Ascending(x => x.IsDeleted)), new CreateIndexModel(Index .Ascending(x => x.ReferencedIds)) }, ct); @@ -77,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents foreach (var entity in contentItems.Result) { - entity.ParseData(schema.SchemaDef, Serializer); + entity.ParseData(schema.SchemaDef, serializer); } return ResultList.Create(contentCount.Result, contentItems.Result); @@ -95,9 +107,35 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } + public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[] status, bool useDraft) + { + var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status)); + + var contentItems = await find.WithoutDraft(useDraft).ToListAsync(); + + var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList(); + var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x))); + + var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); + + foreach (var entity in contentItems) + { + var schema = schemas.FirstOrDefault(x => x.Id == entity.IndexedSchemaId); + + if (schema != null) + { + entity.ParseData(schema.SchemaDef, serializer); + + result.Add((entity, schema)); + } + } + + return result; + } + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, Status[] status, bool useDraft) { - var find = Collection.Find(FilterFactory.Build(schema.Id, ids, status)); + var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status)); var contentItems = find.WithoutDraft(useDraft).ToListAsync(); var contentCount = find.CountDocumentsAsync(); @@ -106,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents foreach (var entity in contentItems.Result) { - entity.ParseData(schema.SchemaDef, Serializer); + entity.ParseData(schema.SchemaDef, serializer); } return ResultList.Create(contentCount.Result, contentItems.Result); @@ -118,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents var contentEntity = await find.WithoutDraft(useDraft).FirstOrDefaultAsync(); - contentEntity?.ParseData(schema.SchemaDef, Serializer); + contentEntity?.ParseData(schema.SchemaDef, serializer); return contentEntity; } @@ -164,7 +202,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); - contentEntity.ParseData(schema.SchemaDef, Serializer); + contentEntity.ParseData(schema.SchemaDef, serializer); return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); } @@ -178,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); - contentEntity.ParseData(schema.SchemaDef, Serializer); + contentEntity.ParseData(schema.SchemaDef, serializer); await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); }, ct); diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 19f46be3a..2a74ffe74 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents this.indexer = indexer; this.serializer = serializer; - contents = new MongoContentCollection(database, serializer); + contents = new MongoContentCollection(database, serializer, appProvider); } public Task InitializeAsync(CancellationToken ct = default) @@ -53,6 +53,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query) { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(status, nameof(status)); + Guard.NotNull(query, nameof(query)); + using (Profiler.TraceMethod("QueryAsyncByQuery")) { var useDraft = UseDraft(status); @@ -70,6 +75,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(status, nameof(status)); + Guard.NotNull(ids, nameof(ids)); + using (Profiler.TraceMethod("QueryAsyncByIds")) { var useDraft = UseDraft(status); @@ -78,8 +88,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } + public async Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids) + { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(status, nameof(status)); + Guard.NotNull(ids, nameof(ids)); + + using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) + { + var useDraft = UseDraft(status); + + return await contents.QueryAsync(app, ids, status, useDraft); + } + } + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id) { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(status, nameof(status)); + using (Profiler.TraceMethod()) { var useDraft = UseDraft(status); @@ -124,7 +152,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents private static bool UseDraft(Status[] status) { - return !(status?.Length == 1 && status[0] == Status.Published); + return status.Length != 1 || status[0] != Status.Published; } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs index 1da6bf8d9..61c256448 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs @@ -117,26 +117,40 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors public static FilterDefinition Build(Guid schemaId, Guid id, Status[] status) { - return CreateFilter(schemaId, new List { id }, status, null); + return CreateFilter(null, schemaId, new List { id }, status, null); } - public static FilterDefinition Build(Guid schemaId, ICollection ids, Status[] status) + public static FilterDefinition IdsByApp(Guid appId, ICollection ids, Status[] status) { - return CreateFilter(schemaId, ids, status, null); + return CreateFilter(appId, null, ids, status, null); + } + + public static FilterDefinition IdsBySchema(Guid schemaId, ICollection ids, Status[] status) + { + return CreateFilter(null, schemaId, ids, status, null); } public static FilterDefinition ToFilter(this Query query, Guid schemaId, ICollection ids, Status[] status) { - return CreateFilter(schemaId, ids, status, query); + return CreateFilter(null, schemaId, ids, status, query); } - private static FilterDefinition CreateFilter(Guid schemaId, ICollection ids, Status[] status, Query query) + private static FilterDefinition CreateFilter(Guid? appId, Guid? schemaId, ICollection ids, Status[] status, Query query) { - var filters = new List> + var filters = new List>(); + + if (appId.HasValue) { - Filter.Eq(x => x.IndexedSchemaId, schemaId), - Filter.Ne(x => x.IsDeleted, true) - }; + filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value)); + } + + if (schemaId.HasValue) + { + filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value)); + } + + filters.Add(Filter.Ne(x => x.IsDeleted, true)); + filters.Add(Filter.In(x => x.Status, status)); if (ids != null && ids.Count > 0) { @@ -150,11 +164,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors } } - if (status != null) - { - filters.Add(Filter.In(x => x.Status, status)); - } - if (query.Filter != null) { filters.Add(query.Filter.BuildFilter()); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index 14570304c..2d90d55db 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -23,6 +23,7 @@ using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries.OData; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; using Squidex.Shared; using Squidex.Shared.Identity; @@ -81,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await GetSchemaAsync(context, schemaIdOrName); - CheckPermission(schema, context.User); + CheckPermission(context.User, schema); using (Profiler.TraceMethod()) { @@ -99,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); } - return Transform(context, schema, true, content); + return Transform(context, schema, content); } } @@ -109,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await GetSchemaAsync(context, schemaIdOrName); - CheckPermission(schema, context.User); + CheckPermission(context.User, schema); using (Profiler.TraceMethod()) { @@ -120,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents if (query.Ids?.Count > 0) { contents = await contentRepository.QueryAsync(context.App, schema, status, new HashSet(query.Ids)); - contents = Sort(contents, query.Ids); + contents = SortSet(contents, query.Ids); } else { @@ -129,34 +130,67 @@ namespace Squidex.Domain.Apps.Entities.Contents contents = await contentRepository.QueryAsync(context.App, schema, status, parsedQuery); } - return Transform(context, schema, true, contents); + return Transform(context, schema, contents); } } - private IContentEntity Transform(QueryContext context, ISchemaEntity schema, bool checkType, IContentEntity content) + public async Task> QueryAsync(QueryContext context, IReadOnlyList ids) { - return TransformCore(context, schema, checkType, Enumerable.Repeat(content, 1)).FirstOrDefault(); + Guard.NotNull(context, nameof(context)); + + using (Profiler.TraceMethod()) + { + var status = GetQueryStatus(context); + + List result; + + if (ids?.Count > 0) + { + var contents = await contentRepository.QueryAsync(context.App, status, new HashSet(ids)); + + var permissions = context.User.Permissions(); + + contents = contents.Where(x => HasPermission(permissions, x.Schema)).ToList(); + + result = contents.Select(x => Transform(context, x.Schema, x.Content)).ToList(); + result = SortList(result, ids).ToList(); + } + else + { + result = new List(); + } + + return result; + } } - private IResultList Transform(QueryContext context, ISchemaEntity schema, bool checkType, IResultList contents) + private IResultList Transform(QueryContext context, ISchemaEntity schema, IResultList contents) { - var transformed = TransformCore(context, schema, checkType, contents); + var transformed = TransformCore(context, schema, contents); return ResultList.Create(contents.Total, transformed); } - private static IResultList Sort(IResultList contents, IReadOnlyList ids) + private IContentEntity Transform(QueryContext context, ISchemaEntity schema, IContentEntity content) { - var sorted = ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null); + return TransformCore(context, schema, Enumerable.Repeat(content, 1)).FirstOrDefault(); + } + + private static IResultList SortSet(IResultList contents, IReadOnlyList ids) + { + return ResultList.Create(contents.Total, SortList(contents, ids)); + } - return ResultList.Create(contents.Total, sorted); + private static IEnumerable SortList(IEnumerable contents, IReadOnlyList ids) + { + return ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null); } - private IEnumerable TransformCore(QueryContext context, ISchemaEntity schema, bool checkType, IEnumerable contents) + private IEnumerable TransformCore(QueryContext context, ISchemaEntity schema, IEnumerable contents) { using (Profiler.TraceMethod()) { - var converters = GenerateConverters(context, checkType).ToArray(); + var converters = GenerateConverters(context).ToArray(); var scriptText = schema.SchemaDef.Scripts.Query; @@ -170,7 +204,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { if (!context.IsFrontendClient && isScripting) { - result.Data = scriptEngine.Transform(new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }, scriptText); + var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; + + result.Data = scriptEngine.Transform(ctx, scriptText); } result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters); @@ -186,7 +222,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private IEnumerable GenerateConverters(QueryContext context, bool checkType) + private IEnumerable GenerateConverters(QueryContext context) { if (!context.IsFrontendClient) { @@ -194,11 +230,8 @@ namespace Squidex.Domain.Apps.Entities.Contents yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); } - if (checkType) - { - yield return FieldConverters.ExcludeChangedTypes(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); - } + yield return FieldConverters.ExcludeChangedTypes(); + yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig); @@ -274,17 +307,26 @@ namespace Squidex.Domain.Apps.Entities.Contents return schema; } - private static void CheckPermission(ISchemaEntity schema, ClaimsPrincipal user) + private static void CheckPermission(ClaimsPrincipal user, params ISchemaEntity[] schemas) { var permissions = user.Permissions(); - var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); - if (!permissions.Allows(permission)) + foreach (var schema in schemas) { - throw new DomainForbiddenException("You do not have permission for this schema."); + if (!HasPermission(permissions, schema)) + { + throw new DomainForbiddenException("You do not have permission for this schema."); + } } } + private static bool HasPermission(PermissionSet permissions, ISchemaEntity schema) + { + var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); + + return permissions.Allows(permission); + } + private static Status[] GetFindStatus(QueryContext context) { if (context.IsFrontendClient) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 0512c3c66..f93bdc1d7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -13,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentQueryService { + Task> QueryAsync(QueryContext context, IReadOnlyList ids); + Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query); Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 7944c1b32..8f5c2f0bd 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories { public interface IContentRepository { + Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids); + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids); Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query); diff --git a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs index df8678dd9..d4e46c533 100644 --- a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs +++ b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs @@ -14,6 +14,6 @@ namespace Squidex.Infrastructure.Assets { Task GetImageInfoAsync(Stream source); - Task CreateThumbnailAsync(Stream source, Stream destination, int? width, int? height, string mode); + Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null); } } diff --git a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs index f36089eeb..929acb63d 100644 --- a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs +++ b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs @@ -9,6 +9,7 @@ using System; using System.IO; using System.Threading.Tasks; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Transforms; using SixLabors.Primitives; @@ -17,49 +18,59 @@ namespace Squidex.Infrastructure.Assets.ImageSharp { public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator { - public ImageSharpAssetThumbnailGenerator() - { - Configuration.Default.ImageFormatsManager.AddImageFormat(ImageFormats.Jpeg); - Configuration.Default.ImageFormatsManager.AddImageFormat(ImageFormats.Png); - } - - public Task CreateThumbnailAsync(Stream source, Stream destination, int? width, int? height, string mode) + public Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null) { return Task.Run(() => { - if (width == null && height == null) + if (!width.HasValue && !height.HasValue && !quality.HasValue) { source.CopyTo(destination); return; } - var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase); - - if (!Enum.TryParse(mode, true, out var resizeMode)) - { - resizeMode = ResizeMode.Max; - } - - if (isCropUpsize) + using (var sourceImage = Image.Load(source, out var format)) { - resizeMode = ResizeMode.Crop; - } + var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); - var w = width ?? 0; - var h = height ?? 0; + if (quality.HasValue) + { + encoder = new JpegEncoder { Quality = quality.Value }; + } - using (var sourceImage = Image.Load(source, out var format)) - { - if (w >= sourceImage.Width && h >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize) + if (encoder == null) { - resizeMode = ResizeMode.BoxPad; + throw new NotSupportedException(); } - var options = new ResizeOptions { Size = new Size(w, h), Mode = resizeMode }; + if (width.HasValue || height.HasValue) + { + var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase); + + if (!Enum.TryParse(mode, true, out var resizeMode)) + { + resizeMode = ResizeMode.Max; + } + + if (isCropUpsize) + { + resizeMode = ResizeMode.Crop; + } + + var resizeWidth = width ?? 0; + var resizeHeight = height ?? 0; + + if (resizeWidth >= sourceImage.Width && resizeHeight >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize) + { + resizeMode = ResizeMode.BoxPad; + } + + var options = new ResizeOptions { Size = new Size(resizeWidth, resizeHeight), Mode = resizeMode }; + + sourceImage.Mutate(x => x.Resize(options)); + } - sourceImage.Mutate(x => x.Resize(options)); - sourceImage.Save(destination, format); + sourceImage.Save(destination, encoder); } }); } diff --git a/src/Squidex.Infrastructure/Languages.cs b/src/Squidex.Infrastructure/Languages.cs index 403b79996..ead00749d 100644 --- a/src/Squidex.Infrastructure/Languages.cs +++ b/src/Squidex.Infrastructure/Languages.cs @@ -197,6 +197,8 @@ namespace Squidex.Infrastructure public static readonly Language AfarEthiopia = AddLanguage("aa-ET", "Afar (Ethiopia)"); public static readonly Language AfrikaansNamibia = AddLanguage("af-NA", "Afrikaans (Namibia)"); public static readonly Language AfrikaansSouthAfrica = AddLanguage("af-ZA", "Afrikaans (South Africa)"); + public static readonly Language AkanGhana = AddLanguage("ak-GH", "Akan (Ghana)"); + public static readonly Language AmharicEthiopia = AddLanguage("am-ET", "Amharic (Ethiopia)"); public static readonly Language ArabicUnitedArabEmirates = AddLanguage("ar-AE", "Arabic (United Arab Emirates)"); public static readonly Language ArabicBahrain = AddLanguage("ar-BH", "Arabic (Bahrain)"); public static readonly Language ArabicDjibouti = AddLanguage("ar-DJ", "Arabic (Djibouti)"); @@ -223,14 +225,23 @@ namespace Squidex.Infrastructure public static readonly Language ArabicChad = AddLanguage("ar-TD", "Arabic (Chad)"); public static readonly Language ArabicTunisia = AddLanguage("ar-TN", "Arabic (Tunisia)"); public static readonly Language ArabicYemen = AddLanguage("ar-YE", "Arabic (Yemen)"); + public static readonly Language AssameseIndia = AddLanguage("as-IN", "Assamese (India)"); + public static readonly Language BashkirRussia = AddLanguage("ba-RU", "Bashkir (Russia)"); + public static readonly Language BelarusianBelarus = AddLanguage("be-BY", "Belarusian (Belarus)"); + public static readonly Language BulgarianBulgaria = AddLanguage("bg-BG", "Bulgarian (Bulgaria)"); public static readonly Language BanglaBangladesh = AddLanguage("bn-BD", "Bangla (Bangladesh)"); public static readonly Language BanglaIndia = AddLanguage("bn-IN", "Bangla (India)"); public static readonly Language TibetanChina = AddLanguage("bo-CN", "Tibetan (China)"); public static readonly Language TibetanIndia = AddLanguage("bo-IN", "Tibetan (India)"); + public static readonly Language BretonFrance = AddLanguage("br-FR", "Breton (France)"); public static readonly Language CatalanAndorra = AddLanguage("ca-AD", "Catalan (Andorra)"); public static readonly Language CatalanCatalan = AddLanguage("ca-ES", "Catalan (Catalan)"); public static readonly Language CatalanFrance = AddLanguage("ca-FR", "Catalan (France)"); public static readonly Language CatalanItaly = AddLanguage("ca-IT", "Catalan (Italy)"); + public static readonly Language ChechenRussia = AddLanguage("ce-RU", "Chechen (Russia)"); + public static readonly Language CorsicanFrance = AddLanguage("co-FR", "Corsican (France)"); + public static readonly Language CzechCzechia = AddLanguage("cs-CZ", "Czech (Czechia)"); + public static readonly Language WelshUnitedKingdom = AddLanguage("cy-GB", "Welsh (United Kingdom)"); public static readonly Language DanishDenmark = AddLanguage("da-DK", "Danish (Denmark)"); public static readonly Language DanishGreenland = AddLanguage("da-GL", "Danish (Greenland)"); public static readonly Language GermanAustria = AddLanguage("de-AT", "German (Austria)"); @@ -240,6 +251,8 @@ namespace Squidex.Infrastructure public static readonly Language GermanItaly = AddLanguage("de-IT", "German (Italy)"); public static readonly Language GermanLiechtenstein = AddLanguage("de-LI", "German (Liechtenstein)"); public static readonly Language GermanLuxembourg = AddLanguage("de-LU", "German (Luxembourg)"); + public static readonly Language DivehiMaldives = AddLanguage("dv-MV", "Divehi (Maldives)"); + public static readonly Language DzongkhaBhutan = AddLanguage("dz-BT", "Dzongkha (Bhutan)"); public static readonly Language EweGhana = AddLanguage("ee-GH", "Ewe (Ghana)"); public static readonly Language EweTogo = AddLanguage("ee-TG", "Ewe (Togo)"); public static readonly Language GreekCyprus = AddLanguage("el-CY", "Greek (Cyprus)"); @@ -371,10 +384,14 @@ namespace Squidex.Infrastructure public static readonly Language SpanishUnitedStates = AddLanguage("es-US", "Spanish (United States)"); public static readonly Language SpanishUruguay = AddLanguage("es-UY", "Spanish (Uruguay)"); public static readonly Language SpanishVenezuela = AddLanguage("es-VE", "Spanish (Venezuela)"); + public static readonly Language EstonianEstonia = AddLanguage("et-EE", "Estonian (Estonia)"); + public static readonly Language BasqueBasque = AddLanguage("eu-ES", "Basque (Basque)"); + public static readonly Language PersianIran = AddLanguage("fa-IR", "Persian (Iran)"); public static readonly Language FulahCameroon = AddLanguage("ff-CM", "Fulah (Cameroon)"); public static readonly Language FulahGuinea = AddLanguage("ff-GN", "Fulah (Guinea)"); public static readonly Language FulahMauritania = AddLanguage("ff-MR", "Fulah (Mauritania)"); public static readonly Language FulahNigeria = AddLanguage("ff-NG", "Fulah (Nigeria)"); + public static readonly Language FinnishFinland = AddLanguage("fi-FI", "Finnish (Finland)"); public static readonly Language FaroeseDenmark = AddLanguage("fo-DK", "Faroese (Denmark)"); public static readonly Language FaroeseFaroeIslands = AddLanguage("fo-FO", "Faroese (Faroe Islands)"); public static readonly Language FrenchBelgium = AddLanguage("fr-BE", "French (Belgium)"); @@ -423,23 +440,63 @@ namespace Squidex.Infrastructure public static readonly Language FrenchVanuatu = AddLanguage("fr-VU", "French (Vanuatu)"); public static readonly Language FrenchWallisandFutuna = AddLanguage("fr-WF", "French (Wallis and Futuna)"); public static readonly Language FrenchMayotte = AddLanguage("fr-YT", "French (Mayotte)"); + public static readonly Language WesternFrisianNetherlands = AddLanguage("fy-NL", "Western Frisian (Netherlands)"); + public static readonly Language IrishIreland = AddLanguage("ga-IE", "Irish (Ireland)"); + public static readonly Language ScottishGaelicUnitedKingdom = AddLanguage("gd-GB", "Scottish Gaelic (United Kingdom)"); + public static readonly Language GalicianGalician = AddLanguage("gl-ES", "Galician (Galician)"); + public static readonly Language GuaraniParaguay = AddLanguage("gn-PY", "Guarani (Paraguay)"); + public static readonly Language GujaratiIndia = AddLanguage("gu-IN", "Gujarati (India)"); + public static readonly Language ManxIsleofMan = AddLanguage("gv-IM", "Manx (Isle of Man)"); + public static readonly Language HebrewIsrael = AddLanguage("he-IL", "Hebrew (Israel)"); + public static readonly Language HindiIndia = AddLanguage("hi-IN", "Hindi (India)"); public static readonly Language CroatianBosniaandHerzegovina = AddLanguage("hr-BA", "Croatian (Bosnia and Herzegovina)"); public static readonly Language CroatianCroatia = AddLanguage("hr-HR", "Croatian (Croatia)"); + public static readonly Language HungarianHungary = AddLanguage("hu-HU", "Hungarian (Hungary)"); + public static readonly Language ArmenianArmenia = AddLanguage("hy-AM", "Armenian (Armenia)"); + public static readonly Language IndonesianIndonesia = AddLanguage("id-ID", "Indonesian (Indonesia)"); + public static readonly Language IgboNigeria = AddLanguage("ig-NG", "Igbo (Nigeria)"); + public static readonly Language YiChina = AddLanguage("ii-CN", "Yi (China)"); + public static readonly Language IcelandicIceland = AddLanguage("is-IS", "Icelandic (Iceland)"); public static readonly Language ItalianSwitzerland = AddLanguage("it-CH", "Italian (Switzerland)"); public static readonly Language ItalianItaly = AddLanguage("it-IT", "Italian (Italy)"); public static readonly Language ItalianSanMarino = AddLanguage("it-SM", "Italian (San Marino)"); public static readonly Language ItalianVaticanCity = AddLanguage("it-VA", "Italian (Vatican City)"); + public static readonly Language JapaneseJapan = AddLanguage("ja-JP", "Japanese (Japan)"); + public static readonly Language GeorgianGeorgia = AddLanguage("ka-GE", "Georgian (Georgia)"); + public static readonly Language KikuyuKenya = AddLanguage("ki-KE", "Kikuyu (Kenya)"); + public static readonly Language KazakhKazakhstan = AddLanguage("kk-KZ", "Kazakh (Kazakhstan)"); + public static readonly Language GreenlandicGreenland = AddLanguage("kl-GL", "Greenlandic (Greenland)"); + public static readonly Language KhmerCambodia = AddLanguage("km-KH", "Khmer (Cambodia)"); + public static readonly Language KannadaIndia = AddLanguage("kn-IN", "Kannada (India)"); public static readonly Language KoreanNorthKorea = AddLanguage("ko-KP", "Korean (North Korea)"); public static readonly Language KoreanKorea = AddLanguage("ko-KR", "Korean (Korea)"); + public static readonly Language KanuriNigeria = AddLanguage("kr-NG", "Kanuri (Nigeria)"); + public static readonly Language CornishUnitedKingdom = AddLanguage("kw-GB", "Cornish (United Kingdom)"); + public static readonly Language KyrgyzKyrgyzstan = AddLanguage("ky-KG", "Kyrgyz (Kyrgyzstan)"); + public static readonly Language LuxembourgishLuxembourg = AddLanguage("lb-LU", "Luxembourgish (Luxembourg)"); + public static readonly Language GandaUganda = AddLanguage("lg-UG", "Ganda (Uganda)"); public static readonly Language LingalaAngola = AddLanguage("ln-AO", "Lingala (Angola)"); public static readonly Language LingalaCongoDRC = AddLanguage("ln-CD", "Lingala (Congo DRC)"); public static readonly Language LingalaCentralAfricanRepublic = AddLanguage("ln-CF", "Lingala (Central African Republic)"); public static readonly Language LingalaCongo = AddLanguage("ln-CG", "Lingala (Congo)"); + public static readonly Language LaoLaos = AddLanguage("lo-LA", "Lao (Laos)"); + public static readonly Language LithuanianLithuania = AddLanguage("lt-LT", "Lithuanian (Lithuania)"); + public static readonly Language LubaKatangaCongoDRC = AddLanguage("lu-CD", "Luba-Katanga (Congo DRC)"); + public static readonly Language LatvianLatvia = AddLanguage("lv-LV", "Latvian (Latvia)"); + public static readonly Language MalagasyMadagascar = AddLanguage("mg-MG", "Malagasy (Madagascar)"); + public static readonly Language MaoriNewZealand = AddLanguage("mi-NZ", "Maori (New Zealand)"); + public static readonly Language MacedonianMacedoniaFYRO = AddLanguage("mk-MK", "Macedonian (Macedonia, FYRO)"); + public static readonly Language MalayalamIndia = AddLanguage("ml-IN", "Malayalam (India)"); + public static readonly Language MongolianMongolia = AddLanguage("mn-MN", "Mongolian (Mongolia)"); + public static readonly Language MarathiIndia = AddLanguage("mr-IN", "Marathi (India)"); public static readonly Language MalayBrunei = AddLanguage("ms-BN", "Malay (Brunei)"); public static readonly Language MalayMalaysia = AddLanguage("ms-MY", "Malay (Malaysia)"); public static readonly Language MalaySingapore = AddLanguage("ms-SG", "Malay (Singapore)"); + public static readonly Language MalteseMalta = AddLanguage("mt-MT", "Maltese (Malta)"); + public static readonly Language BurmeseMyanmar = AddLanguage("my-MM", "Burmese (Myanmar)"); public static readonly Language NorwegianBokmålNorway = AddLanguage("nb-NO", "Norwegian Bokmål (Norway)"); public static readonly Language NorwegianBokmålSvalbardandJanMayen = AddLanguage("nb-SJ", "Norwegian Bokmål (Svalbard and Jan Mayen)"); + public static readonly Language NorthNdebeleZimbabwe = AddLanguage("nd-ZW", "North Ndebele (Zimbabwe)"); public static readonly Language NepaliIndia = AddLanguage("ne-IN", "Nepali (India)"); public static readonly Language NepaliNepal = AddLanguage("ne-NP", "Nepali (Nepal)"); public static readonly Language DutchAruba = AddLanguage("nl-AW", "Dutch (Aruba)"); @@ -449,8 +506,13 @@ namespace Squidex.Infrastructure public static readonly Language DutchNetherlands = AddLanguage("nl-NL", "Dutch (Netherlands)"); public static readonly Language DutchSuriname = AddLanguage("nl-SR", "Dutch (Suriname)"); public static readonly Language DutchSintMaarten = AddLanguage("nl-SX", "Dutch (Sint Maarten)"); + public static readonly Language NorwegianNynorskNorway = AddLanguage("nn-NO", "Norwegian Nynorsk (Norway)"); + public static readonly Language SouthNdebeleSouthAfrica = AddLanguage("nr-ZA", "South Ndebele (South Africa)"); public static readonly Language OromoEthiopia = AddLanguage("om-ET", "Oromo (Ethiopia)"); public static readonly Language OromoKenya = AddLanguage("om-KE", "Oromo (Kenya)"); + public static readonly Language OdiaIndia = AddLanguage("or-IN", "Odia (India)"); + public static readonly Language PolishPoland = AddLanguage("pl-PL", "Polish (Poland)"); + public static readonly Language PashtoAfghanistan = AddLanguage("ps-AF", "Pashto (Afghanistan)"); public static readonly Language PortugueseAngola = AddLanguage("pt-AO", "Portuguese (Angola)"); public static readonly Language PortugueseBrazil = AddLanguage("pt-BR", "Portuguese (Brazil)"); public static readonly Language PortugueseSwitzerland = AddLanguage("pt-CH", "Portuguese (Switzerland)"); @@ -463,6 +525,8 @@ namespace Squidex.Infrastructure public static readonly Language PortuguesePortugal = AddLanguage("pt-PT", "Portuguese (Portugal)"); public static readonly Language PortugueseSãoToméandPríncipe = AddLanguage("pt-ST", "Portuguese (São Tomé and Príncipe)"); public static readonly Language PortugueseTimorLeste = AddLanguage("pt-TL", "Portuguese (Timor-Leste)"); + public static readonly Language RomanshSwitzerland = AddLanguage("rm-CH", "Romansh (Switzerland)"); + public static readonly Language RundiBurundi = AddLanguage("rn-BI", "Rundi (Burundi)"); public static readonly Language RomanianMoldova = AddLanguage("ro-MD", "Romanian (Moldova)"); public static readonly Language RomanianRomania = AddLanguage("ro-RO", "Romanian (Romania)"); public static readonly Language RussianBelarus = AddLanguage("ru-BY", "Russian (Belarus)"); @@ -471,9 +535,15 @@ namespace Squidex.Infrastructure public static readonly Language RussianMoldova = AddLanguage("ru-MD", "Russian (Moldova)"); public static readonly Language RussianRussia = AddLanguage("ru-RU", "Russian (Russia)"); public static readonly Language RussianUkraine = AddLanguage("ru-UA", "Russian (Ukraine)"); + public static readonly Language KinyarwandaRwanda = AddLanguage("rw-RW", "Kinyarwanda (Rwanda)"); + public static readonly Language SanskritIndia = AddLanguage("sa-IN", "Sanskrit (India)"); public static readonly Language SamiNorthernFinland = AddLanguage("se-FI", "Sami, Northern (Finland)"); public static readonly Language SamiNorthernNorway = AddLanguage("se-NO", "Sami, Northern (Norway)"); public static readonly Language SamiNorthernSweden = AddLanguage("se-SE", "Sami, Northern (Sweden)"); + public static readonly Language SangoCentralAfricanRepublic = AddLanguage("sg-CF", "Sango (Central African Republic)"); + public static readonly Language SinhalaSriLanka = AddLanguage("si-LK", "Sinhala (Sri Lanka)"); + public static readonly Language SlovakSlovakia = AddLanguage("sk-SK", "Slovak (Slovakia)"); + public static readonly Language SlovenianSlovenia = AddLanguage("sl-SI", "Slovenian (Slovenia)"); public static readonly Language SomaliDjibouti = AddLanguage("so-DJ", "Somali (Djibouti)"); public static readonly Language SomaliEthiopia = AddLanguage("so-ET", "Somali (Ethiopia)"); public static readonly Language SomaliKenya = AddLanguage("so-KE", "Somali (Kenya)"); @@ -496,14 +566,26 @@ namespace Squidex.Infrastructure public static readonly Language TamilSriLanka = AddLanguage("ta-LK", "Tamil (Sri Lanka)"); public static readonly Language TamilMalaysia = AddLanguage("ta-MY", "Tamil (Malaysia)"); public static readonly Language TamilSingapore = AddLanguage("ta-SG", "Tamil (Singapore)"); + public static readonly Language TeluguIndia = AddLanguage("te-IN", "Telugu (India)"); + public static readonly Language ThaiThailand = AddLanguage("th-TH", "Thai (Thailand)"); public static readonly Language TigrinyaEritrea = AddLanguage("ti-ER", "Tigrinya (Eritrea)"); public static readonly Language TigrinyaEthiopia = AddLanguage("ti-ET", "Tigrinya (Ethiopia)"); + public static readonly Language TurkmenTurkmenistan = AddLanguage("tk-TM", "Turkmen (Turkmenistan)"); public static readonly Language SetswanaBotswana = AddLanguage("tn-BW", "Setswana (Botswana)"); public static readonly Language SetswanaSouthAfrica = AddLanguage("tn-ZA", "Setswana (South Africa)"); + public static readonly Language TonganTonga = AddLanguage("to-TO", "Tongan (Tonga)"); public static readonly Language TurkishCyprus = AddLanguage("tr-CY", "Turkish (Cyprus)"); public static readonly Language TurkishTurkey = AddLanguage("tr-TR", "Turkish (Turkey)"); + public static readonly Language XitsongaSouthAfrica = AddLanguage("ts-ZA", "Xitsonga (South Africa)"); + public static readonly Language TatarRussia = AddLanguage("tt-RU", "Tatar (Russia)"); + public static readonly Language UyghurChina = AddLanguage("ug-CN", "Uyghur (China)"); + public static readonly Language UkrainianUkraine = AddLanguage("uk-UA", "Ukrainian (Ukraine)"); public static readonly Language UrduIndia = AddLanguage("ur-IN", "Urdu (India)"); public static readonly Language UrduPakistan = AddLanguage("ur-PK", "Urdu (Pakistan)"); + public static readonly Language VendaSouthAfrica = AddLanguage("ve-ZA", "Venda (South Africa)"); + public static readonly Language VietnameseVietnam = AddLanguage("vi-VN", "Vietnamese (Vietnam)"); + public static readonly Language WolofSenegal = AddLanguage("wo-SN", "Wolof (Senegal)"); + public static readonly Language isiXhosaSouthAfrica = AddLanguage("xh-ZA", "isiXhosa (South Africa)"); public static readonly Language YorubaBenin = AddLanguage("yo-BJ", "Yoruba (Benin)"); public static readonly Language YorubaNigeria = AddLanguage("yo-NG", "Yoruba (Nigeria)"); public static readonly Language ChineseSimplifiedChina = AddLanguage("zh-CN", "Chinese (Simplified, China)"); @@ -511,5 +593,6 @@ namespace Squidex.Infrastructure public static readonly Language ChineseTraditionalMacaoSAR = AddLanguage("zh-MO", "Chinese (Traditional, Macao SAR)"); public static readonly Language ChineseSimplifiedSingapore = AddLanguage("zh-SG", "Chinese (Simplified, Singapore)"); public static readonly Language ChineseTraditionalTaiwan = AddLanguage("zh-TW", "Chinese (Traditional, Taiwan)"); + public static readonly Language isiZuluSouthAfrica = AddLanguage("zu-ZA", "isiZulu (South Africa)"); } } diff --git a/src/Squidex.Web/Constants.cs b/src/Squidex.Web/Constants.cs index 23535e548..770c8a435 100644 --- a/src/Squidex.Web/Constants.cs +++ b/src/Squidex.Web/Constants.cs @@ -23,6 +23,8 @@ namespace Squidex.Web public static readonly string PortalPrefix = "/portal"; + public static readonly string EmailScope = "email"; + public static readonly string RoleScope = "role"; public static readonly string PermissionsScope = "permissions"; diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 9475c951d..cf71326c0 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -51,6 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// The optional version of the asset. /// The target width of the asset, if it is an image. /// The target height of the asset, if it is an image. + /// Optional image quality, it is is an jpeg image. /// The resize mode when the width and height is defined. /// /// 200 => Asset found and content or (resized) image returned. @@ -64,11 +65,12 @@ namespace Squidex.Areas.Api.Controllers.Assets [FromQuery] long version = EtagVersion.Any, [FromQuery] int? width = null, [FromQuery] int? height = null, + [FromQuery] int? quality = null, [FromQuery] string mode = null) { var entity = await assetRepository.FindAssetAsync(id); - if (entity == null || entity.FileVersion < version || width == 0 || height == 0) + if (entity == null || entity.FileVersion < version || width == 0 || height == 0 || quality == 0) { return NotFound(); } @@ -79,10 +81,15 @@ namespace Squidex.Areas.Api.Controllers.Assets { var assetId = entity.Id.ToString(); - if (entity.IsImage && (width.HasValue || height.HasValue)) + if (entity.IsImage && (width.HasValue || height.HasValue || quality.HasValue)) { var assetSuffix = $"{width}_{height}_{mode}"; + if (quality.HasValue) + { + assetSuffix += $"_{quality}"; + } + try { await assetStore.DownloadAsync(assetId, entity.FileVersion, assetSuffix, bodyStream); @@ -103,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Assets using (Profiler.Trace("ResizeImage")) { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode); + await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode, quality); destinationStream.Position = 0; } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index ec36149e1..f5467dfad 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -106,6 +106,45 @@ namespace Squidex.Areas.Api.Controllers.Contents } } + /// + /// Queries contents. + /// + /// The name of the app. + /// The optional ids of the content to fetch. + /// Indicates whether to query content items from the archive. + /// + /// 200 => Contents retrieved. + /// 404 => App not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs + /// + [HttpGet] + [Route("content/{app}/")] + [ApiPermission] + [ApiCosts(1)] + public async Task GetAllContents(string app, [FromQuery] string ids, [FromQuery] bool archived = false) + { + var context = Context().WithArchived(archived); + + var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids); + + var response = new ContentsDto + { + Total = result.Count, + Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray() + }; + + if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys(); + } + + Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag(); + + return Ok(response); + } + /// /// Queries contents. /// @@ -124,7 +163,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [Route("content/{app}/{name}/")] [ApiPermission] [ApiCosts(1)] - public async Task GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null) + public async Task GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] bool archived = false) { var context = Context().WithArchived(archived); diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs b/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs index de5b21262..652c61f79 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; @@ -20,7 +21,14 @@ namespace Squidex.Areas.IdentityServer.Controllers { var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); - externalLogin.ProviderDisplayName = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; + var email = externalLogin.Principal.FindFirst(ClaimTypes.Email)?.Value; + + if (string.IsNullOrWhiteSpace(email)) + { + throw new InvalidOperationException("External provider does not provide email claim."); + } + + externalLogin.ProviderDisplayName = email; return externalLogin; } @@ -28,6 +36,7 @@ namespace Squidex.Areas.IdentityServer.Controllers public static async Task> GetExternalProvidersAsync(this SignInManager signInManager) { var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); + var externalProviders = externalSchemes.Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme) .Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList(); diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml b/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml index 9e76e4eb0..54b6042d0 100644 --- a/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml +++ b/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml @@ -36,7 +36,7 @@
} diff --git a/src/Squidex/Config/Authentication/OidcServices.cs b/src/Squidex/Config/Authentication/OidcServices.cs index 533baa295..de3eebe6a 100644 --- a/src/Squidex/Config/Authentication/OidcServices.cs +++ b/src/Squidex/Config/Authentication/OidcServices.cs @@ -25,6 +25,7 @@ namespace Squidex.Config.Authentication options.Authority = identityOptions.OidcAuthority; options.ClientId = identityOptions.OidcClient; options.ClientSecret = identityOptions.OidcSecret; + options.Scope.Add(Constants.EmailScope); options.Scope.Add(Constants.PermissionsScope); options.RequireHttpsMetadata = false; }); diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 5f9f788db..b1deb7ae3 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -250,6 +250,9 @@ namespace Squidex.Config.Domain services.AddTransientAs() .AsSelf(); + services.AddTransientAs() + .AsSelf(); + services.AddTransientAs() .As(); diff --git a/src/Squidex/Config/Startup/RebuilderHost.cs b/src/Squidex/Config/Startup/RebuilderHost.cs new file mode 100644 index 000000000..55877619b --- /dev/null +++ b/src/Squidex/Config/Startup/RebuilderHost.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Migrate_01; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class RebuilderHost : SafeHostedService + { + private readonly RebuildRunner rebuildRunner; + + public RebuilderHost(IApplicationLifetime lifetime, ISemanticLog log, RebuildRunner rebuildRunner) + : base(lifetime, log) + { + this.rebuildRunner = rebuildRunner; + } + + protected override Task StartAsync(ISemanticLog log, CancellationToken ct) + { + return rebuildRunner.RunAsync(ct); + } + } +} diff --git a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/src/Squidex/Pipeline/Squid/SquidMiddleware.cs index 8bf5d0fb7..a9f2e305a 100644 --- a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ b/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -83,13 +83,16 @@ namespace Squidex.Pipeline.Squid svg = svg.Replace("{{TEXT3}}", l3); svg = svg.Replace("[COLOR]", background); + context.Response.StatusCode = 200; context.Response.ContentType = "image/svg+xml"; context.Response.Headers["Cache-Control"] = "public, max-age=604800"; await context.Response.WriteAsync(svg); } - - await next(context); + else + { + await next(context); + } } private static (string, string, string) SplitText(string text) diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 769c5774c..29a9c580f 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Migrate_01; using Squidex.Areas.Api; using Squidex.Areas.Api.Config.Swagger; using Squidex.Areas.Api.Controllers.Contents; @@ -91,6 +92,8 @@ namespace Squidex config.GetSection("urls")); services.Configure( config.GetSection("usage")); + services.Configure( + config.GetSection("rebuild")); services.Configure( config.GetSection("contentsController")); @@ -107,6 +110,7 @@ namespace Squidex services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); return services.BuildServiceProvider(); } diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 09e72705c..705f1273f 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -355,5 +355,28 @@ * The deepl api key if you want to support automated translations. */ "deeplAuthKey": "" + }, + + "rebuild": { + /* + * Set to true to rebuild apps. + */ + "apps": false, + /* + * Set to true to rebuild assets. + */ + "assets": false, + /* + * Set to true to rebuild contents. + */ + "contents": false, + /* + * Set to true to rebuild rules. + */ + "rules": false, + /* + * Set to true to rebuild schemas. + */ + "schemas": false } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs index 8a23d1e14..d062a2f97 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs @@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent Fields.Json(1, "1", Partitioning.Invariant, new JsonFieldProperties()); - Assert.Equal(JsonValue.Object(), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index d8035841b..cbf1b5d0b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -358,7 +358,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Theory] [MemberData(nameof(ManyIdDataApi))] - public async Task Should_query_contents_by_id_from_repository_and_transform(bool archive, bool unpublished, params Status[] status) + public async Task Should_query_contents_by_id_for_api_and_transform(bool archive, bool unpublished, params Status[] status) { const int count = 5, total = 200; @@ -380,6 +380,86 @@ namespace Squidex.Domain.Apps.Entities.Contents .MustHaveHappened(count, Times.Exactly); } + [Theory] + [MemberData(nameof(ManyIdDataFrontend))] + public async Task Should_query_all_contents_by_id_for_frontend_and_transform(bool archive, bool unpublished, params Status[] status) + { + const int count = 5; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupClaims(true); + SetupSchema(); + SetupScripting(ids.ToArray()); + SetupContents(status, ids); + + var ctx = context.WithArchived(archive).WithUnpublished(unpublished); + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ManyIdDataApi))] + public async Task Should_query_all_contents_by_id_for_api_and_transform(bool archive, bool unpublished, params Status[] status) + { + const int count = 5; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupClaims(); + SetupSchema(); + SetupScripting(ids.ToArray()); + SetupContents(status, ids); + + var ctx = context.WithArchived(archive).WithUnpublished(unpublished); + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(count, Times.Exactly); + } + + [Fact] + public async Task Should_skip_contents_when_user_has_no_permission() + { + var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList(); + + SetupClaims(false, false); + SetupSchema(); + SetupContents(new Status[0], ids); + + var ctx = context; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Empty(result); + } + + [Fact] + public async Task Should_not_call_repository_if_no_id_defined() + { + var ids = new List(); + + SetupClaims(false, false); + SetupSchema(); + + var ctx = context; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Empty(result); + + A.CallTo(() => contentRepository.QueryAsync(app, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + private void SetupClaims(bool isFrontend = false, bool allowSchema = true) { if (isFrontend) @@ -414,6 +494,12 @@ namespace Squidex.Domain.Apps.Entities.Contents .Returns(ResultList.Create(total, ids.Select(x => CreateContent(x)).Shuffle())); } + private void SetupContents(Status[] status, List ids) + { + A.CallTo(() => contentRepository.QueryAsync(app, A.That.IsSameSequenceAs(status), A>.Ignored)) + .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); + } + private void SetupSchema() { A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) diff --git a/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs index 798707593..f9f75a47d 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs @@ -15,16 +15,15 @@ namespace Squidex.Infrastructure.Assets { public class ImageSharpAssetThumbnailGeneratorTests { - private const string Image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTM0A1t6AAAADElEQVQYV2P4//8/AAX+Av6nNYGEAAAAAElFTkSuQmCC"; private readonly ImageSharpAssetThumbnailGenerator sut = new ImageSharpAssetThumbnailGenerator(); + private readonly MemoryStream target = new MemoryStream(); [Fact] public async Task Should_return_same_image_if_no_size_is_passed_for_thumbnail() { - var source = new MemoryStream(Convert.FromBase64String(Image)); - var target = new MemoryStream(); + var source = GetPng(); - await sut.CreateThumbnailAsync(source, target, null, null, "resize"); + await sut.CreateThumbnailAsync(source, target); Assert.Equal(target.Length, source.Length); } @@ -32,23 +31,42 @@ namespace Squidex.Infrastructure.Assets [Fact] public async Task Should_resize_image_to_target() { - var source = new MemoryStream(Convert.FromBase64String(Image)); - var target = new MemoryStream(); + var source = GetPng(); - await sut.CreateThumbnailAsync(source, target, 100, 100, "resize"); + await sut.CreateThumbnailAsync(source, target, 1000, 1000, "resize"); Assert.True(target.Length > source.Length); } + [Fact] + public async Task Should_change_jpeg_quality_and_write_to_target() + { + var source = GetJpeg(); + + await sut.CreateThumbnailAsync(source, target, quality: 10); + + Assert.True(target.Length < source.Length); + } + + [Fact] + public async Task Should_change_png_quality_and_write_to_target() + { + var source = GetPng(); + + await sut.CreateThumbnailAsync(source, target, quality: 10); + + Assert.True(target.Length < source.Length); + } + [Fact] public async Task Should_return_image_information_if_image_is_valid() { - var source = new MemoryStream(Convert.FromBase64String(Image)); + var source = GetPng(); var imageInfo = await sut.GetImageInfoAsync(source); - Assert.Equal(1, imageInfo.PixelHeight); - Assert.Equal(1, imageInfo.PixelWidth); + Assert.Equal(600, imageInfo.PixelHeight); + Assert.Equal(600, imageInfo.PixelWidth); } [Fact] @@ -60,5 +78,15 @@ namespace Squidex.Infrastructure.Assets Assert.Null(imageInfo); } + + private Stream GetPng() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.png"); + } + + private Stream GetJpeg() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg"); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg b/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg new file mode 100644 index 000000000..e5395ad0a Binary files /dev/null and b/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg differ diff --git a/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png b/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png new file mode 100644 index 000000000..3cbc19038 Binary files /dev/null and b/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png differ diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index c5d586c5c..a50b7fc40 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -6,6 +6,10 @@ Squidex.Infrastructure 7.3 + + + + @@ -35,4 +39,8 @@ + + + + \ No newline at end of file diff --git a/tools/GenerateLanguages/Program.cs b/tools/GenerateLanguages/Program.cs index 6911b350d..7cd25828e 100644 --- a/tools/GenerateLanguages/Program.cs +++ b/tools/GenerateLanguages/Program.cs @@ -83,9 +83,6 @@ namespace GenerateLanguages return CultureInfo.GetCultures(CultureTypes.SpecificCultures) .Where(x => x.ToString().Length == 5) .Where(x => languages.Any(l => l.Iso2Code.Equals(x.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))) - .GroupBy(x => x.TwoLetterISOLanguageName) - .Where(x => x.Count() > 1) - .SelectMany(x => x) .Select(x => (x.ToString(), x.EnglishName)); } diff --git a/tools/Migrate_01/Migrations/BuildFullTextIndices.cs b/tools/Migrate_01/Migrations/BuildFullTextIndices.cs index b4f2dbb7a..9cda18d1c 100644 --- a/tools/Migrate_01/Migrations/BuildFullTextIndices.cs +++ b/tools/Migrate_01/Migrations/BuildFullTextIndices.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Squidex.Domain.Apps.Entities.Contents.State; @@ -27,7 +28,12 @@ namespace Migrate_01.Migrations this.store = store; } - public async Task UpdateAsync() + public Task UpdateAsync() + { + return UpdateAsync(); + } + + public async Task UpdateAsync(CancellationToken ct) { var snapshotStore = store.GetSnapshotStore(); @@ -40,7 +46,7 @@ namespace Migrate_01.Migrations MaxDegreeOfParallelism = Environment.ProcessorCount * 2 }); - await snapshotStore.ReadAllAsync((state, version) => worker.SendAsync(state)); + await snapshotStore.ReadAllAsync((state, version) => worker.SendAsync(state), ct); worker.Complete(); diff --git a/tools/Migrate_01/RebuildOptions.cs b/tools/Migrate_01/RebuildOptions.cs new file mode 100644 index 000000000..9b1bb1a63 --- /dev/null +++ b/tools/Migrate_01/RebuildOptions.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Migrate_01 +{ + public sealed class RebuildOptions + { + public bool Apps { get; set; } + + public bool Assets { get; set; } + + public bool Contents { get; set; } + + public bool Indices { get; set; } + + public bool Rules { get; set; } + + public bool Schemas { get; set; } + } +} diff --git a/tools/Migrate_01/RebuildRunner.cs b/tools/Migrate_01/RebuildRunner.cs new file mode 100644 index 000000000..f180ecc78 --- /dev/null +++ b/tools/Migrate_01/RebuildRunner.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Migrate_01.Migrations; +using Squidex.Infrastructure; + +namespace Migrate_01 +{ + public sealed class RebuildRunner + { + private readonly Rebuilder rebuilder; + private readonly BuildFullTextIndices fullTextIndices; + private readonly RebuildOptions rebuildOptions; + + public RebuildRunner(Rebuilder rebuilder, BuildFullTextIndices fullTextIndices, IOptions rebuildOptions) + { + Guard.NotNull(rebuilder, nameof(rebuilder)); + Guard.NotNull(rebuildOptions, nameof(rebuildOptions)); + Guard.NotNull(fullTextIndices, nameof(fullTextIndices)); + + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + this.fullTextIndices = fullTextIndices; + } + + public async Task RunAsync(CancellationToken ct) + { + if (rebuildOptions.Apps) + { + await rebuilder.RebuildAppsAsync(ct); + } + + if (rebuildOptions.Schemas) + { + await rebuilder.RebuildSchemasAsync(ct); + } + + if (rebuildOptions.Rules) + { + await rebuilder.RebuildRulesAsync(ct); + } + + if (rebuildOptions.Assets) + { + await rebuilder.RebuildAssetsAsync(ct); + } + + if (rebuildOptions.Contents) + { + await rebuilder.RebuildContentAsync(ct); + } + + if (rebuildOptions.Indices) + { + await fullTextIndices.UpdateAsync(ct); + } + } + } +} diff --git a/tools/Migrate_01/Rebuilder.cs b/tools/Migrate_01/Rebuilder.cs index 7a3a81811..8f0eaa062 100644 --- a/tools/Migrate_01/Rebuilder.cs +++ b/tools/Migrate_01/Rebuilder.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Squidex.Domain.Apps.Entities.Apps; @@ -43,32 +44,32 @@ namespace Migrate_01 this.store = store; } - public Task RebuildAppsAsync() + public Task RebuildAppsAsync(CancellationToken ct = default) { - return RebuildManyAsync("^app\\-"); + return RebuildManyAsync("^app\\-", ct); } - public Task RebuildSchemasAsync() + public Task RebuildSchemasAsync(CancellationToken ct = default) { - return RebuildManyAsync("^schema\\-"); + return RebuildManyAsync("^schema\\-", ct); } - public Task RebuildRulesAsync() + public Task RebuildRulesAsync(CancellationToken ct = default) { - return RebuildManyAsync("^rule\\-"); + return RebuildManyAsync("^rule\\-", ct); } - public Task RebuildAssetsAsync() + public Task RebuildAssetsAsync(CancellationToken ct = default) { - return RebuildManyAsync("^asset\\-"); + return RebuildManyAsync("^asset\\-", ct); } - public Task RebuildContentAsync() + public Task RebuildContentAsync(CancellationToken ct = default) { - return RebuildManyAsync("^content\\-"); + return RebuildManyAsync("^content\\-", ct); } - private async Task RebuildManyAsync(string filter) where TState : IDomainState, new() + private async Task RebuildManyAsync(string filter, CancellationToken ct) where TState : IDomainState, new() { var handledIds = new HashSet(); @@ -113,7 +114,7 @@ namespace Migrate_01 { await worker.SendAsync(id); } - }, filter); + }, filter, ct); worker.Complete();