diff --git a/Directory.Build.props b/Directory.Build.props index 78d22a3b4e..d3143be7a8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,7 +22,6 @@ true - true true snupkg @@ -37,7 +36,6 @@ - diff --git a/global.json b/global.json index 4c4c3ae5ed..da113e4cbd 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "8.0.100", + "version": "8.0.0", "rollForward": "latestFeature", "allowPrerelease": false } -} +} \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props new file mode 100644 index 0000000000..ba1a67f988 --- /dev/null +++ b/src/Directory.Packages.props @@ -0,0 +1,72 @@ + + + + true + NU1507 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj index 0a42debdf0..523d6f4982 100644 --- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj +++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj @@ -9,11 +9,11 @@ - - - - - + + + + + diff --git a/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs b/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs new file mode 100644 index 0000000000..da1580554c --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.OutputCaching; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DeliveryApi; + +namespace Umbraco.Cms.Api.Delivery.Caching; + +internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy +{ + private readonly TimeSpan _duration; + + public DeliveryApiOutputCachePolicy(TimeSpan duration) + => _duration = duration; + + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) + { + IRequestPreviewService requestPreviewService = context + .HttpContext + .RequestServices + .GetRequiredService(); + + context.EnableOutputCaching = requestPreviewService.IsPreview() is false; + context.ResponseExpirationTimeSpan = _duration; + + return ValueTask.CompletedTask; + } + + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) + => ValueTask.CompletedTask; + + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) + => ValueTask.CompletedTask; +} diff --git a/src/Umbraco.Cms.Api.Delivery/Caching/OutputCachePipelineFilter.cs b/src/Umbraco.Cms.Api.Delivery/Caching/OutputCachePipelineFilter.cs new file mode 100644 index 0000000000..89ff10462c --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Caching/OutputCachePipelineFilter.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Builder; +using Umbraco.Cms.Web.Common.ApplicationBuilder; + +namespace Umbraco.Cms.Api.Delivery.Caching; + +internal sealed class OutputCachePipelineFilter : UmbracoPipelineFilter +{ + public OutputCachePipelineFilter(string name) + : base(name) + => PostPipeline = PostPipelineAction; + + private void PostPipelineAction(IApplicationBuilder applicationBuilder) + => applicationBuilder.UseOutputCache(); +} diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs index 405da6e15f..4637055c11 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Delivery.Filters; using Umbraco.Cms.Api.Delivery.Routing; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Services.OperationStatus; @@ -13,6 +15,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content; [ApiExplorerSettings(GroupName = "Content")] [LocalizeFromAcceptLanguageHeader] [ValidateStartItem] +[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)] public abstract class ContentApiControllerBase : DeliveryApiControllerBase { protected IApiPublishedContentCache ApiPublishedContentCache { get; } diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs index 73a385fd2e..5a9bc4763e 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Delivery.Filters; using Umbraco.Cms.Api.Delivery.Routing; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -14,6 +16,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media; [DeliveryApiMediaAccess] [VersionedDeliveryApiRoute("media")] [ApiExplorerSettings(GroupName = "Media")] +[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.MediaCachePolicy)] public abstract class MediaApiControllerBase : DeliveryApiControllerBase { private readonly IApiMediaWithCropsResponseBuilder _apiMediaWithCropsResponseBuilder; diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs index f17fc14773..9f56f591ce 100644 --- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs @@ -3,9 +3,11 @@ using System.Text.Json.Serialization; using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.DependencyInjection; using Umbraco.Cms.Api.Delivery.Accessors; +using Umbraco.Cms.Api.Delivery.Caching; using Umbraco.Cms.Api.Delivery.Configuration; using Umbraco.Cms.Api.Delivery.Handlers; using Umbraco.Cms.Api.Delivery.Json; @@ -14,10 +16,12 @@ using Umbraco.Cms.Api.Delivery.Routing; using Umbraco.Cms.Api.Delivery.Security; using Umbraco.Cms.Api.Delivery.Services; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.Security; +using Umbraco.Cms.Web.Common.ApplicationBuilder; namespace Umbraco.Extensions; @@ -79,7 +83,38 @@ public static class UmbracoBuilderExtensions // FIXME: remove this when Delivery API V1 is removed builder.Services.AddSingleton(); + + builder.AddOutputCache(); + return builder; + } + + private static IUmbracoBuilder AddOutputCache(this IUmbracoBuilder builder) + { + DeliveryApiSettings.OutputCacheSettings outputCacheSettings = + builder.Config.GetSection(Constants.Configuration.ConfigDeliveryApi).Get()?.OutputCache + ?? new DeliveryApiSettings.OutputCacheSettings(); + + if (outputCacheSettings.Enabled is false || outputCacheSettings is { ContentDuration.TotalSeconds: <= 0, MediaDuration.TotalSeconds: <= 0 }) + { + return builder; + } + + builder.Services.AddOutputCache(options => + { + options.AddBasePolicy(_ => { }); + + if (outputCacheSettings.ContentDuration.TotalSeconds > 0) + { + options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration)); + } + + if (outputCacheSettings.MediaDuration.TotalSeconds > 0) + { + options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration)); + } + }); + + builder.Services.Configure(options => options.AddFilter(new OutputCachePipelineFilter("UmbracoDeliveryApiOutputCache"))); return builder; } } - diff --git a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj index 431f73cd32..37aa74414c 100644 --- a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj +++ b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj index db7ac432bc..bb9b44cf51 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj @@ -2,11 +2,12 @@ Umbraco CMS - Imaging - ImageSharp Adds imaging support using ImageSharp/ImageSharp.Web to Umbraco CMS. + false - - + + diff --git a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj index 14c203bad6..d55479d8ec 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj @@ -2,11 +2,12 @@ Umbraco CMS - Imaging - ImageSharp 2 Adds imaging support using ImageSharp/ImageSharp.Web version 2 to Umbraco CMS. + false - - + + diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj index baab56e96e..5229f513a2 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj index 84e88c3523..36751bb869 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj @@ -1,12 +1,11 @@ - + Umbraco CMS - Persistence - Entity Framework Core - SQLite migrations Adds support for Entity Framework Core SQLite migrations to Umbraco CMS. - - + diff --git a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj index d2c8c64405..39f035407b 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj @@ -5,10 +5,9 @@ - - - - + + + diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj index 37cd0da0f7..0de13a39b6 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj +++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj index b49b4eebee..f9755aad61 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj +++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj index e8ceb6b216..4ce8f99e64 100644 --- a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj +++ b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj @@ -6,6 +6,7 @@ false false + false diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj index 39a3b03c56..8b0dcb8a87 100644 --- a/src/Umbraco.Cms/Umbraco.Cms.csproj +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -4,6 +4,7 @@ Installs Umbraco CMS with all default dependencies in your ASP.NET Core project. false false + false diff --git a/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs b/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs index c81102b8d2..1f75d47055 100644 --- a/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs @@ -59,6 +59,11 @@ public class DeliveryApiSettings /// public MemberAuthorizationSettings? MemberAuthorization { get; set; } = null; + /// + /// Gets or sets the settings for the Delivery API output cache. + /// + public OutputCacheSettings OutputCache { get; set; } = new (); + /// /// Gets a value indicating if any member authorization type is enabled for the Delivery API. /// @@ -138,4 +143,42 @@ public class DeliveryApiSettings /// These are only required if logout is to be used. public Uri[] LogoutRedirectUrls { get; set; } = Array.Empty(); } + + /// + /// Typed configuration options for output caching of the Delivery API. + /// + public class OutputCacheSettings + { + private const string StaticDuration = "00:01:00"; // one minute + + /// + /// Gets or sets a value indicating whether the Delivery API output should be cached. + /// + /// true if the Delivery API output should be cached; otherwise, false. + /// + /// The default value is false. + /// + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; + + /// + /// Gets or sets a value indicating how long the Content Delivery API output should be cached. + /// + /// Cache lifetime. + /// + /// The default cache duration is one minute, if this configuration value is not provided. + /// + [DefaultValue(StaticDuration)] + public TimeSpan ContentDuration { get; set; } = TimeSpan.Parse(StaticDuration); + + /// + /// Gets or sets a value indicating how long the Media Delivery API output should be cached. + /// + /// Cache lifetime. + /// + /// The default cache duration is one minute, if this configuration value is not provided. + /// + [DefaultValue(StaticDuration)] + public TimeSpan MediaDuration { get; set; } = TimeSpan.Parse(StaticDuration); + } } diff --git a/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs b/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs index 2bfb5a2375..781fd942b5 100644 --- a/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs @@ -8,6 +8,9 @@ public class WebhookSettings private const bool StaticEnabled = true; private const int StaticMaximumRetries = 5; internal const string StaticPeriod = "00:00:10"; + private const bool StaticEnableLoggingCleanup = true; + private const int StaticKeepLogsForDays = 30; + /// /// Gets or sets a value indicating whether webhooks are enabled. @@ -38,4 +41,26 @@ public class WebhookSettings /// [DefaultValue(StaticPeriod)] public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod); + + /// + /// Gets or sets a value indicating whether cleanup of webhook logs are enabled. + /// + /// + /// + /// By default, cleanup is enabled. + /// + /// + [DefaultValue(StaticEnableLoggingCleanup)] + public bool EnableLoggingCleanup { get; set; } = StaticEnableLoggingCleanup; + + /// + /// Gets or sets a value indicating number of days to keep logs for. + /// + /// + /// + /// By default, logs are kept for 30 days. + /// + /// + [DefaultValue(StaticKeepLogsForDays)] + public int KeepLogsForDays { get; set; } = StaticKeepLogsForDays; } diff --git a/src/Umbraco.Core/Constants-DeliveryApi.cs b/src/Umbraco.Core/Constants-DeliveryApi.cs index e2f23e414c..85677a23bc 100644 --- a/src/Umbraco.Core/Constants-DeliveryApi.cs +++ b/src/Umbraco.Core/Constants-DeliveryApi.cs @@ -17,5 +17,21 @@ public static partial class Constants /// public const string PreviewContentPathPrefix = "preview-"; } + + /// + /// Constants for Delivery API output cache. + /// + public static class OutputCache + { + /// + /// Output cache policy name for content + /// + public const string ContentCachePolicy = "DeliveryApiContent"; + + /// + /// Output cache policy name for media + /// + public const string MediaCachePolicy = "DeliveryApiMedia"; + } } } diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs index fda84c1013..9ddaeb22a5 100644 --- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs @@ -8,146 +8,126 @@ using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Extensions; /// -/// Provides extension methods that return udis for Umbraco entities. +/// Provides extension methods that return udis for Umbraco entities. /// public static class UdiGetterExtensions { /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this ITemplate entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IContentType entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.DocumentType, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IMediaType entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.MediaType, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IMemberType entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IMemberGroup entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IContentTypeComposition entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); - string type; - if (entity is IContentType) + string entityType = entity switch { - type = Constants.UdiEntityType.DocumentType; - } - else if (entity is IMediaType) - { - type = Constants.UdiEntityType.MediaType; - } - else if (entity is IMemberType) - { - type = Constants.UdiEntityType.MemberType; - } - else - { - throw new NotSupportedException(string.Format( - "Composition type {0} is not supported.", - entity.GetType().FullName)); - } + IContentType => Constants.UdiEntityType.DocumentType, + IMediaType => Constants.UdiEntityType.MediaType, + IMemberType => Constants.UdiEntityType.MemberType, + _ => throw new NotSupportedException(string.Format("Composition type {0} is not supported.", entity.GetType().FullName)), + }; - return new GuidUdi(type, entity.Key).EnsureClosed(); + return new GuidUdi(entityType, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IDataType entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this EntityContainer entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); string entityType; if (entity.ContainedObjectType == Constants.ObjectTypes.DataType) @@ -164,339 +144,250 @@ public static class UdiGetterExtensions } else { - throw new NotSupportedException(string.Format( - "Contained object type {0} is not supported.", - entity.ContainedObjectType)); + throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType)); } return new GuidUdi(entityType, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IMedia entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IContent entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); - return new GuidUdi( - entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document, - entity.Key) - .EnsureClosed(); + string entityType = entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document; + + return new GuidUdi(entityType, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IMember entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static StringUdi GetUdi(this Stylesheet entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static StringUdi GetUdi(this Script entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path); } + /// + /// Gets the UDI from a path. + /// + /// The type of the entity. + /// The path. + /// + /// The entity identifier of the entity. + /// private static StringUdi GetUdiFromPath(string entityType, string path) { - var id = path - .TrimStart(Constants.CharArrays.ForwardSlash) - .Replace("\\", "/"); + string id = path.TrimStart(Constants.CharArrays.ForwardSlash).Replace("\\", "/"); + return new StringUdi(entityType, id).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IDictionaryItem entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.DictionaryItem, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IMacro entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.Macro, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static StringUdi GetUdi(this IPartialView entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); - // we should throw on Unknown but for the time being, assume it means PartialView - var entityType = entity.ViewType == PartialViewType.PartialViewMacro - ? Constants.UdiEntityType.PartialViewMacro - : Constants.UdiEntityType.PartialView; + // TODO: We should throw on Unknown, but for the time being, assume it means PartialView + string entityType = entity.ViewType switch + { + PartialViewType.PartialViewMacro => Constants.UdiEntityType.PartialViewMacro, + _ => Constants.UdiEntityType.PartialView, + }; return GetUdiFromPath(entityType, entity.Path); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IContentBase entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); - string type; - if (entity is IContent) + string type = entity switch { - type = Constants.UdiEntityType.Document; - } - else if (entity is IMedia) - { - type = Constants.UdiEntityType.Media; - } - else if (entity is IMember) - { - type = Constants.UdiEntityType.Member; - } - else - { - throw new NotSupportedException(string.Format( - "ContentBase type {0} is not supported.", - entity.GetType().FullName)); - } + IContent => Constants.UdiEntityType.Document, + IMedia => Constants.UdiEntityType.Media, + IMember => Constants.UdiEntityType.Member, + _ => throw new NotSupportedException(string.Format("Content base type {0} is not supported.", entity.GetType().FullName)), + }; return new GuidUdi(type, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static GuidUdi GetUdi(this IRelationType entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this Webhook entity) + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IWebhook entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new GuidUdi(Constants.UdiEntityType.Webhook, entity.Key).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static StringUdi GetUdi(this ILanguage entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); } /// - /// Gets the entity identifier of the entity. + /// Gets the entity identifier of the entity. /// /// The entity. - /// The entity identifier of the entity. + /// + /// The entity identifier of the entity. + /// public static Udi GetUdi(this IEntity entity) { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } + ArgumentNullException.ThrowIfNull(entity); - // entity could eg be anything implementing IThing - // so we have to go through casts here - if (entity is ITemplate template) + return entity switch { - return template.GetUdi(); - } - - if (entity is IContentType contentType) - { - return contentType.GetUdi(); - } - - if (entity is IMediaType mediaType) - { - return mediaType.GetUdi(); - } - - if (entity is IMemberType memberType) - { - return memberType.GetUdi(); - } - - if (entity is IMemberGroup memberGroup) - { - return memberGroup.GetUdi(); - } - - if (entity is IContentTypeComposition contentTypeComposition) - { - return contentTypeComposition.GetUdi(); - } - - if (entity is IDataType dataTypeComposition) - { - return dataTypeComposition.GetUdi(); - } - - if (entity is EntityContainer container) - { - return container.GetUdi(); - } - - if (entity is IMedia media) - { - return media.GetUdi(); - } - - if (entity is IContent content) - { - return content.GetUdi(); - } - - if (entity is IMember member) - { - return member.GetUdi(); - } - - if (entity is Stylesheet stylesheet) - { - return stylesheet.GetUdi(); - } - - if (entity is Script script) - { - return script.GetUdi(); - } - - if (entity is IDictionaryItem dictionaryItem) - { - return dictionaryItem.GetUdi(); - } - - if (entity is IMacro macro) - { - return macro.GetUdi(); - } - - if (entity is IPartialView partialView) - { - return partialView.GetUdi(); - } - - if (entity is IContentBase contentBase) - { - return contentBase.GetUdi(); - } - - if (entity is IRelationType relationType) - { - return relationType.GetUdi(); - } - - if (entity is ILanguage language) - { - return language.GetUdi(); - } - - throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); + // Concrete types + EntityContainer container => container.GetUdi(), + Stylesheet stylesheet => stylesheet.GetUdi(), + Script script => script.GetUdi(), + // Content types + IContentType contentType => contentType.GetUdi(), + IMediaType mediaType => mediaType.GetUdi(), + IMemberType memberType => memberType.GetUdi(), + IContentTypeComposition contentTypeComposition => contentTypeComposition.GetUdi(), + // Content + IContent content => content.GetUdi(), + IMedia media => media.GetUdi(), + IMember member => member.GetUdi(), + IContentBase contentBase => contentBase.GetUdi(), + // Other + IDataType dataTypeComposition => dataTypeComposition.GetUdi(), + IDictionaryItem dictionaryItem => dictionaryItem.GetUdi(), + ILanguage language => language.GetUdi(), + IMacro macro => macro.GetUdi(), + IMemberGroup memberGroup => memberGroup.GetUdi(), + IPartialView partialView => partialView.GetUdi(), + IRelationType relationType => relationType.GetUdi(), + ITemplate template => template.GetUdi(), + IWebhook webhook => webhook.GetUdi(), + _ => throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)), + }; } } diff --git a/src/Umbraco.Core/Models/IWebhook.cs b/src/Umbraco.Core/Models/IWebhook.cs new file mode 100644 index 0000000000..ab8c6ed05d --- /dev/null +++ b/src/Umbraco.Core/Models/IWebhook.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models; + +public interface IWebhook : IEntity +{ + string Url { get; set; } + + string[] Events { get; set; } + + Guid[] ContentTypeKeys {get; set; } + + bool Enabled { get; set; } + + IDictionary Headers { get; set; } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index 09e9a00389..009666aab5 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -60,5 +60,5 @@ public interface IPublishedContentTypeFactory /// This is so the factory can flush its caches. /// Invoked by the IPublishedSnapshotService. /// - void NotifyDataTypeChanges(int[] ids); + void NotifyDataTypeChanges(params int[] ids); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 2216bb3d1b..32fab1b539 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -10,8 +10,8 @@ public class PublishedContentTypeFactory : IPublishedContentTypeFactory { private readonly IDataTypeService _dataTypeService; private readonly PropertyValueConverterCollection _propertyValueConverters; - private readonly object _publishedDataTypesLocker = new(); private readonly IPublishedModelFactory _publishedModelFactory; + private object _publishedDataTypesLocker = new(); private Dictionary? _publishedDataTypes; public PublishedContentTypeFactory( @@ -52,19 +52,12 @@ public class PublishedContentTypeFactory : IPublishedContentTypeFactory /// public PublishedDataType GetDataType(int id) { - Dictionary? publishedDataTypes; - lock (_publishedDataTypesLocker) - { - if (_publishedDataTypes == null) - { - IEnumerable dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); - } + Dictionary publishedDataTypes = LazyInitializer.EnsureInitialized( + ref _publishedDataTypes, + ref _publishedDataTypesLocker, + () => _dataTypeService.GetAll().ToDictionary(x => x.Id, CreatePublishedDataType)); - publishedDataTypes = _publishedDataTypes; - } - - if (publishedDataTypes is null || !publishedDataTypes.TryGetValue(id, out PublishedDataType? dataType)) + if (!publishedDataTypes.TryGetValue(id, out PublishedDataType? dataType)) { throw new ArgumentException($"Could not find a datatype with identifier {id}.", nameof(id)); } @@ -73,24 +66,31 @@ public class PublishedContentTypeFactory : IPublishedContentTypeFactory } /// - public void NotifyDataTypeChanges(int[] ids) + public void NotifyDataTypeChanges(params int[] ids) { + if (_publishedDataTypes is null) + { + // Not initialized yet, so skip and avoid lock + return; + } + lock (_publishedDataTypesLocker) { - if (_publishedDataTypes == null) + if (ids.Length == 0) { - IEnumerable dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); + // Clear cache (and let it lazy initialize again later) + _publishedDataTypes = null; } else { + // Remove items from cache (in case the data type is removed) foreach (var id in ids) { _publishedDataTypes.Remove(id); } - IEnumerable dataTypes = _dataTypeService.GetAll(ids); - foreach (IDataType dataType in dataTypes) + // Update cacheB + foreach (IDataType dataType in _dataTypeService.GetAll(ids)) { _publishedDataTypes[dataType.Id] = CreatePublishedDataType(dataType); } diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index bc31745cf5..499cee0186 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -1,27 +1,71 @@ -namespace Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Extensions; -public class Webhook +namespace Umbraco.Cms.Core.Models; + +public class Webhook : EntityBase, IWebhook { + // Custom comparers for enumerable + private static readonly DelegateEqualityComparer> + ContentTypeKeysComparer = + new( + (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), + enumerable => enumerable.GetHashCode()); + + private static readonly DelegateEqualityComparer> + EventsComparer = + new( + (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), + enumerable => enumerable.GetHashCode()); + + private static readonly DelegateEqualityComparer> + HeadersComparer = + new( + (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), + enumerable => enumerable.GetHashCode()); + + private string _url; + private string[] _events; + private Guid[] _contentTypeKeys; + private bool _enabled; + private IDictionary _headers; + public Webhook(string url, bool? enabled = null, Guid[]? entityKeys = null, string[]? events = null, IDictionary? headers = null) { - Url = url; - Headers = headers ?? new Dictionary(); - Events = events ?? Array.Empty(); - ContentTypeKeys = entityKeys ?? Array.Empty(); - Enabled = enabled ?? false; + _url = url; + _headers = headers ?? new Dictionary(); + _events = events ?? Array.Empty(); + _contentTypeKeys = entityKeys ?? Array.Empty(); + _enabled = enabled ?? false; } - public int Id { get; set; } + public string Url + { + get => _url; + set => SetPropertyValueAndDetectChanges(value, ref _url!, nameof(Url)); + } - public Guid Key { get; set; } + public string[] Events + { + get => _events; + set => SetPropertyValueAndDetectChanges(value, ref _events!, nameof(Events), EventsComparer); + } - public string Url { get; set; } + public Guid[] ContentTypeKeys + { + get => _contentTypeKeys; + set => SetPropertyValueAndDetectChanges(value, ref _contentTypeKeys!, nameof(ContentTypeKeys), ContentTypeKeysComparer); + } - public string[] Events { get; set; } + public bool Enabled + { + get => _enabled; + set => SetPropertyValueAndDetectChanges(value, ref _enabled, nameof(Enabled)); + } - public Guid[] ContentTypeKeys {get; set; } - - public bool Enabled { get; set; } - - public IDictionary Headers { get; set; } + public IDictionary Headers + { + get => _headers; + set => SetPropertyValueAndDetectChanges(value, ref _headers!, nameof(Headers), HeadersComparer); + } } diff --git a/src/Umbraco.Core/Notifications/WebhookDeletedNotification .cs b/src/Umbraco.Core/Notifications/WebhookDeletedNotification .cs index 516d52012c..eb4394e59c 100644 --- a/src/Umbraco.Core/Notifications/WebhookDeletedNotification .cs +++ b/src/Umbraco.Core/Notifications/WebhookDeletedNotification .cs @@ -3,9 +3,9 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Notifications; -public class WebhookDeletedNotification : DeletedNotification +public class WebhookDeletedNotification : DeletedNotification { - public WebhookDeletedNotification(Webhook target, EventMessages messages) + public WebhookDeletedNotification(IWebhook target, EventMessages messages) : base(target, messages) { } diff --git a/src/Umbraco.Core/Notifications/WebhookDeletingNotification.cs b/src/Umbraco.Core/Notifications/WebhookDeletingNotification.cs index f703113370..7e5c8e4579 100644 --- a/src/Umbraco.Core/Notifications/WebhookDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/WebhookDeletingNotification.cs @@ -3,14 +3,14 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Notifications; -public class WebhookDeletingNotification : DeletingNotification +public class WebhookDeletingNotification : DeletingNotification { - public WebhookDeletingNotification(Webhook target, EventMessages messages) + public WebhookDeletingNotification(IWebhook target, EventMessages messages) : base(target, messages) { } - public WebhookDeletingNotification(IEnumerable target, EventMessages messages) + public WebhookDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) { } diff --git a/src/Umbraco.Core/Notifications/WebhookSavedNotification.cs b/src/Umbraco.Core/Notifications/WebhookSavedNotification.cs index efd4fc3707..d94b87873e 100644 --- a/src/Umbraco.Core/Notifications/WebhookSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/WebhookSavedNotification.cs @@ -3,14 +3,14 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Notifications; -public class WebhookSavedNotification : SavedNotification +public class WebhookSavedNotification : SavedNotification { - public WebhookSavedNotification(Webhook target, EventMessages messages) + public WebhookSavedNotification(IWebhook target, EventMessages messages) : base(target, messages) { } - public WebhookSavedNotification(IEnumerable target, EventMessages messages) + public WebhookSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) { } diff --git a/src/Umbraco.Core/Notifications/WebhookSavingNotification.cs b/src/Umbraco.Core/Notifications/WebhookSavingNotification.cs index 69dee928c8..8d0a6ce7ff 100644 --- a/src/Umbraco.Core/Notifications/WebhookSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/WebhookSavingNotification.cs @@ -3,14 +3,14 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Notifications; -public class WebhookSavingNotification : SavingNotification +public class WebhookSavingNotification : SavingNotification { - public WebhookSavingNotification(Webhook target, EventMessages messages) + public WebhookSavingNotification(IWebhook target, EventMessages messages) : base(target, messages) { } - public WebhookSavingNotification(IEnumerable target, EventMessages messages) + public WebhookSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) { } diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs index 7672d73ec7..874c0ffe2f 100644 --- a/src/Umbraco.Core/Persistence/Constants-Locks.cs +++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs @@ -72,8 +72,13 @@ public static partial class Constants public const int ScheduledPublishing = -341; /// - /// ScheduledPublishing job. + /// All Webhook requests. /// public const int WebhookRequest = -342; + + /// + /// All webhook logs. + /// + public const int WebhookLogs = -343; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs index a4652d5955..c719df2b50 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs @@ -1,5 +1,4 @@ using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Webhooks; namespace Umbraco.Cms.Core.Persistence.Repositories; @@ -8,4 +7,8 @@ public interface IWebhookLogRepository Task CreateAsync(WebhookLog log); Task> GetPagedAsync(int skip, int take); + + Task> GetOlderThanDate(DateTime date); + + Task DeleteByIds(int[] ids); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs index 3013ee59e0..5fe0530b73 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs @@ -9,43 +9,43 @@ public interface IWebhookRepository /// /// Number of entries to skip. /// Number of entries to take. - /// A paged model of objects. - Task> GetAllAsync(int skip, int take); + /// A paged model of objects. + Task> GetAllAsync(int skip, int take); /// /// Gets all of the webhooks in the current database. /// /// The webhook you want to create. - /// The created webhook - Task CreateAsync(Webhook webhook); + /// The created webhook + Task CreateAsync(IWebhook webhook); /// /// Gets a webhook by key /// /// The key of the webhook which will be retrieved. - /// The webhook with the given key. - Task GetAsync(Guid key); + /// The webhook with the given key. + Task GetAsync(Guid key); /// /// Gets a webhook by key /// /// The alias of an event, which is referenced by a webhook. /// - /// A paged model of + /// A paged model of /// - Task> GetByAliasAsync(string alias); + Task> GetByAliasAsync(string alias); /// /// Gets a webhook by key /// /// The webhook to be deleted. /// A representing the asynchronous operation. - Task DeleteAsync(Webhook webhook); + Task DeleteAsync(IWebhook webhook); /// /// Updates a given webhook /// /// The webhook to be updated. - /// The updated webhook. - Task UpdateAsync(Webhook webhook); + /// The updated webhook. + Task UpdateAsync(IWebhook webhook); } diff --git a/src/Umbraco.Core/Services/IWebhookFiringService.cs b/src/Umbraco.Core/Services/IWebhookFiringService.cs index 53a631bff3..47d01da4ec 100644 --- a/src/Umbraco.Core/Services/IWebhookFiringService.cs +++ b/src/Umbraco.Core/Services/IWebhookFiringService.cs @@ -4,5 +4,5 @@ namespace Umbraco.Cms.Core.Services; public interface IWebhookFiringService { - Task FireAsync(Webhook webhook, string eventAlias, object? payload, CancellationToken cancellationToken); + Task FireAsync(IWebhook webhook, string eventAlias, object? payload, CancellationToken cancellationToken); } diff --git a/src/Umbraco.Core/Services/IWebhookLogFactory.cs b/src/Umbraco.Core/Services/IWebhookLogFactory.cs index 3f586f5da4..feaa12ef4a 100644 --- a/src/Umbraco.Core/Services/IWebhookLogFactory.cs +++ b/src/Umbraco.Core/Services/IWebhookLogFactory.cs @@ -5,5 +5,5 @@ namespace Umbraco.Cms.Core.Services; public interface IWebhookLogFactory { - Task CreateAsync(string eventAlias, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken); + Task CreateAsync(string eventAlias, WebhookResponseModel responseModel, IWebhook webhook, CancellationToken cancellationToken); } diff --git a/src/Umbraco.Core/Services/IWebhookService.cs b/src/Umbraco.Core/Services/IWebhookService.cs index 657f29df59..2fcd3eef94 100644 --- a/src/Umbraco.Core/Services/IWebhookService.cs +++ b/src/Umbraco.Core/Services/IWebhookService.cs @@ -8,34 +8,34 @@ public interface IWebhookService /// /// Creates a webhook. /// - /// to create. - Task> CreateAsync(Webhook webhook); + /// to create. + Task> CreateAsync(IWebhook webhook); /// /// Updates a webhook. /// - /// to update. - Task> UpdateAsync(Webhook webhook); + /// to update. + Task> UpdateAsync(IWebhook webhook); /// /// Deletes a webhook. /// /// The unique key of the webhook. - Task> DeleteAsync(Guid key); + Task> DeleteAsync(Guid key); /// /// Gets a webhook by its key. /// /// The unique key of the webhook. - Task GetAsync(Guid key); + Task GetAsync(Guid key); /// /// Gets all webhooks. /// - Task> GetAllAsync(int skip, int take); + Task> GetAllAsync(int skip, int take); /// /// Gets webhooks by event name. /// - Task> GetByAliasAsync(string alias); + Task> GetByAliasAsync(string alias); } diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 9def2bd8fa..b28e0aaaee 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -32,6 +32,7 @@ public class ServiceContext private readonly Lazy? _serverRegistrationService; private readonly Lazy? _tagService; private readonly Lazy? _userService; + private readonly Lazy? _webhookService; /// /// Initializes a new instance of the class with lazy services. @@ -63,7 +64,8 @@ public class ServiceContext Lazy? redirectUrlService, Lazy? consentService, Lazy? keyValueService, - Lazy? contentTypeBaseServiceProvider) + Lazy? contentTypeBaseServiceProvider, + Lazy? webhookService) { _publicAccessService = publicAccessService; _domainService = domainService; @@ -92,6 +94,7 @@ public class ServiceContext _consentService = consentService; _keyValueService = keyValueService; _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _webhookService = webhookService; } /// @@ -229,6 +232,11 @@ public class ServiceContext /// public IContentTypeBaseServiceProvider? ContentTypeBaseServices => _contentTypeBaseServiceProvider?.Value; + /// + /// Gets the WebhookService. + /// + public IWebhookService? WebhookService => _webhookService?.Value; + /// /// Creates a partial service context with only some services (for tests). /// @@ -262,7 +270,8 @@ public class ServiceContext IRedirectUrlService? redirectUrlService = null, IConsentService? consentService = null, IKeyValueService? keyValueService = null, - IContentTypeBaseServiceProvider? contentTypeBaseServiceProvider = null) + IContentTypeBaseServiceProvider? contentTypeBaseServiceProvider = null, + IWebhookService? webhookService = null) { Lazy? Lazy(T? service) { @@ -296,6 +305,8 @@ public class ServiceContext Lazy(redirectUrlService), Lazy(consentService), Lazy(keyValueService), - Lazy(contentTypeBaseServiceProvider)); + Lazy(contentTypeBaseServiceProvider), + Lazy(webhookService) + ); } } diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 761b924ac5..ecce4f167f 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -8,6 +8,11 @@ namespace Umbraco.Extensions; public static class UserServiceExtensions { public static EntityPermission? GetPermissions(this IUserService userService, IUser? user, string path) + { + return userService.GetAllPermissions(user, path).FirstOrDefault(); + } + + public static EntityPermissionCollection GetAllPermissions(this IUserService userService, IUser? user, string path) { var ids = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(x => @@ -23,7 +28,7 @@ public static class UserServiceExtensions " could not be parsed into an array of integers or the path was empty"); } - return userService.GetPermissions(user, ids[^1]).FirstOrDefault(); + return userService.GetPermissions(user, ids[^1]); } /// diff --git a/src/Umbraco.Core/Services/WebhookLogFactory.cs b/src/Umbraco.Core/Services/WebhookLogFactory.cs index 455bc45e27..ec88bb52c4 100644 --- a/src/Umbraco.Core/Services/WebhookLogFactory.cs +++ b/src/Umbraco.Core/Services/WebhookLogFactory.cs @@ -1,11 +1,12 @@ -using Umbraco.Cms.Core.Models; +using System.Net; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Webhooks; namespace Umbraco.Cms.Core.Services; public class WebhookLogFactory : IWebhookLogFactory { - public async Task CreateAsync(string eventAlias, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken) + public async Task CreateAsync(string eventAlias, WebhookResponseModel responseModel, IWebhook webhook, CancellationToken cancellationToken) { var log = new WebhookLog { @@ -20,7 +21,7 @@ public class WebhookLogFactory : IWebhookLogFactory { log.RequestBody = await responseModel.HttpResponseMessage!.RequestMessage!.Content!.ReadAsStringAsync(cancellationToken); log.ResponseBody = await responseModel.HttpResponseMessage.Content.ReadAsStringAsync(cancellationToken); - log.StatusCode = responseModel.HttpResponseMessage.StatusCode.ToString(); + log.StatusCode = MapStatusCodeToMessage(responseModel.HttpResponseMessage.StatusCode); log.RetryCount = responseModel.RetryCount; log.ResponseHeaders = responseModel.HttpResponseMessage.Headers.ToString(); log.RequestHeaders = responseModel.HttpResponseMessage.RequestMessage.Headers.ToString(); @@ -28,4 +29,6 @@ public class WebhookLogFactory : IWebhookLogFactory return log; } + + private string MapStatusCodeToMessage(HttpStatusCode statusCode) => $"{statusCode.ToString()} ({(int)statusCode})"; } diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 08383b3c9d..5f660eb37d 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -20,7 +20,7 @@ public class WebhookService : IWebhookService _eventMessagesFactory = eventMessagesFactory; } - private WebhookOperationStatus ValidateWebhook(Webhook webhook) + private WebhookOperationStatus ValidateWebhook(IWebhook webhook) { if (webhook.Events.Length <= 0) { @@ -31,7 +31,7 @@ public class WebhookService : IWebhookService } /// - public async Task> CreateAsync(Webhook webhook) + public async Task> CreateAsync(IWebhook webhook) { WebhookOperationStatus validationResult = ValidateWebhook(webhook); if (validationResult is not WebhookOperationStatus.Success) @@ -49,7 +49,7 @@ public class WebhookService : IWebhookService return Attempt.FailWithStatus(WebhookOperationStatus.CancelledByNotification, webhook); } - Webhook created = await _webhookRepository.CreateAsync(webhook); + IWebhook created = await _webhookRepository.CreateAsync(webhook); scope.Notifications.Publish(new WebhookSavedNotification(webhook, eventMessages).WithStateFrom(savingNotification)); @@ -59,7 +59,7 @@ public class WebhookService : IWebhookService } /// - public async Task> UpdateAsync(Webhook webhook) + public async Task> UpdateAsync(IWebhook webhook) { WebhookOperationStatus validationResult = ValidateWebhook(webhook); if (validationResult is not WebhookOperationStatus.Success) @@ -69,7 +69,7 @@ public class WebhookService : IWebhookService using ICoreScope scope = _provider.CreateCoreScope(); - Webhook? currentWebhook = await _webhookRepository.GetAsync(webhook.Key); + IWebhook? currentWebhook = await _webhookRepository.GetAsync(webhook.Key); if (currentWebhook is null) { @@ -101,10 +101,10 @@ public class WebhookService : IWebhookService } /// - public async Task> DeleteAsync(Guid key) + public async Task> DeleteAsync(Guid key) { using ICoreScope scope = _provider.CreateCoreScope(); - Webhook? webhook = await _webhookRepository.GetAsync(key); + IWebhook? webhook = await _webhookRepository.GetAsync(key); if (webhook is null) { return Attempt.FailWithStatus(WebhookOperationStatus.NotFound, webhook); @@ -115,7 +115,7 @@ public class WebhookService : IWebhookService if (await scope.Notifications.PublishCancelableAsync(deletingNotification)) { scope.Complete(); - return Attempt.FailWithStatus(WebhookOperationStatus.CancelledByNotification, webhook); + return Attempt.FailWithStatus(WebhookOperationStatus.CancelledByNotification, webhook); } await _webhookRepository.DeleteAsync(webhook); @@ -123,33 +123,33 @@ public class WebhookService : IWebhookService scope.Complete(); - return Attempt.SucceedWithStatus(WebhookOperationStatus.Success, webhook); + return Attempt.SucceedWithStatus(WebhookOperationStatus.Success, webhook); } /// - public async Task GetAsync(Guid key) + public async Task GetAsync(Guid key) { using ICoreScope scope = _provider.CreateCoreScope(); - Webhook? webhook = await _webhookRepository.GetAsync(key); + IWebhook? webhook = await _webhookRepository.GetAsync(key); scope.Complete(); return webhook; } /// - public async Task> GetAllAsync(int skip, int take) + public async Task> GetAllAsync(int skip, int take) { using ICoreScope scope = _provider.CreateCoreScope(); - PagedModel webhooks = await _webhookRepository.GetAllAsync(skip, take); + PagedModel webhooks = await _webhookRepository.GetAllAsync(skip, take); scope.Complete(); return webhooks; } /// - public async Task> GetByAliasAsync(string alias) + public async Task> GetByAliasAsync(string alias) { using ICoreScope scope = _provider.CreateCoreScope(); - PagedModel webhooks = await _webhookRepository.GetByAliasAsync(alias); + PagedModel webhooks = await _webhookRepository.GetByAliasAsync(alias); scope.Complete(); return webhooks.Items; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7a45a2ea14..8aebb53f60 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -7,23 +7,16 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 529fe4191b..5d4ee69d3b 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -53,9 +53,9 @@ public abstract class WebhookEventBase : IWebhookEvent, INotifica /// /// Process the webhooks for the given notification. /// - public virtual async Task ProcessWebhooks(TNotification notification, IEnumerable webhooks, CancellationToken cancellationToken) + public virtual async Task ProcessWebhooks(TNotification notification, IEnumerable webhooks, CancellationToken cancellationToken) { - foreach (Webhook webhook in webhooks) + foreach (IWebhook webhook in webhooks) { if (webhook.Enabled is false) { @@ -91,7 +91,7 @@ public abstract class WebhookEventBase : IWebhookEvent, INotifica return; } - IEnumerable webhooks = await WebhookService.GetByAliasAsync(Alias); + IEnumerable webhooks = await WebhookService.GetByAliasAsync(Alias); await ProcessWebhooks(notification, webhooks, cancellationToken); } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs index 6b72bbaacb..e022f6c0a5 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs @@ -21,9 +21,9 @@ public abstract class WebhookEventContentBase : WebhookE { } - public override async Task ProcessWebhooks(TNotification notification, IEnumerable webhooks, CancellationToken cancellationToken) + public override async Task ProcessWebhooks(TNotification notification, IEnumerable webhooks, CancellationToken cancellationToken) { - foreach (Webhook webhook in webhooks) + foreach (IWebhook webhook in webhooks) { if (!webhook.Enabled) { diff --git a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj index c96686f41c..76349f9671 100644 --- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj +++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs index fe8cf1e204..11eaa3bb8a 100644 --- a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Net.Mime; +using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -65,7 +66,7 @@ public class WebhookFiring : IRecurringBackgroundJob { return Task.Run(async () => { - Webhook? webhook = await _webHookService.GetAsync(request.WebhookKey); + IWebhook? webhook = await _webHookService.GetAsync(request.WebhookKey); if (webhook is null) { return; @@ -87,12 +88,11 @@ public class WebhookFiring : IRecurringBackgroundJob })); } - private async Task SendRequestAsync(Webhook webhook, string eventName, object? payload, int retryCount, CancellationToken cancellationToken) + private async Task SendRequestAsync(IWebhook webhook, string eventName, string? serializedObject, int retryCount, CancellationToken cancellationToken) { using var httpClient = new HttpClient(); - var serializedObject = _jsonSerializer.Serialize(payload); - var stringContent = new StringContent(serializedObject, Encoding.UTF8, "application/json"); + var stringContent = new StringContent(serializedObject ?? string.Empty, Encoding.UTF8, MediaTypeNames.Application.Json); stringContent.Headers.TryAddWithoutValidation("Umb-Webhook-Event", eventName); foreach (KeyValuePair header in webhook.Headers) diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookLoggingCleanup.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookLoggingCleanup.cs new file mode 100644 index 0000000000..2e0bdc5edd --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookLoggingCleanup.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +/// +/// Daily background job that removes all webhook log data older than x days as defined by +/// +public class WebhookLoggingCleanup : IRecurringBackgroundJob +{ + private readonly ILogger _logger; + private readonly WebhookSettings _webhookSettings; + private readonly IWebhookLogRepository _webhookLogRepository; + private readonly ICoreScopeProvider _coreScopeProvider; + + public WebhookLoggingCleanup(ILogger logger, IOptionsMonitor webhookSettings, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) + { + _logger = logger; + _webhookSettings = webhookSettings.CurrentValue; + _webhookLogRepository = webhookLogRepository; + _coreScopeProvider = coreScopeProvider; + } + + /// + // No-op event as the period never changes on this job + public event EventHandler PeriodChanged + { + add { } remove { } + } + + /// + public TimeSpan Period => TimeSpan.FromDays(1); + + /// + public TimeSpan Delay { get; } = TimeSpan.FromSeconds(20); + + /// + public async Task RunJobAsync() + { + if (_webhookSettings.EnableLoggingCleanup is false) + { + _logger.LogInformation("WebhookLoggingCleanup task will not run as it has been globally disabled via configuration"); + return; + } + + IEnumerable webhookLogs; + using (ICoreScope scope = _coreScopeProvider.CreateCoreScope()) + { + scope.ReadLock(Constants.Locks.WebhookLogs); + webhookLogs = await _webhookLogRepository.GetOlderThanDate(DateTime.Now - TimeSpan.FromDays(_webhookSettings.KeepLogsForDays)); + scope.Complete(); + } + + foreach (IEnumerable group in webhookLogs.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + scope.WriteLock(Constants.Locks.WebhookLogs); + await _webhookLogRepository.DeleteByIds(group.Select(x => x.Id).ToArray()); + + scope.Complete(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs index 53ecef211a..76ae3ea13c 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -3,7 +3,9 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Web.Common.DependencyInjection; using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; +using StaticServiceProvider = Umbraco.Cms.Core.DependencyInjection.StaticServiceProvider; namespace Umbraco.Cms.Core.Logging.Viewer; @@ -60,14 +62,15 @@ public class LogViewerConfig : ILogViewerConfig { using IScope scope = _scopeProvider.CreateScope(); ILogViewerQuery? item = _logViewerQueryRepository.GetByName(name); - if (item is not null) { _logViewerQueryRepository.Delete(item); } // Return the updated object - so we can instantly reset the entire array from the API response - IReadOnlyList result = GetSavedSearches(); + IReadOnlyList result = GetSavedSearches()!; + scope.Complete(); + return result; scope.Complete(); return result; } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index f1263d19fe..875ea5b45e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1006,6 +1006,7 @@ internal class DatabaseDataCreator _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.WebhookRequest, Name = "WebhookRequest" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.WebhookLogs, Name = "WebhookLogs" }); } private void CreateContentTypeData() diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 77eed2a38b..d89fe9c0b4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -101,6 +101,9 @@ public class UmbracoPlan : MigrationPlan To("{D5139400-E507-4259-A542-C67358F7E329}"); To("{4E652F18-9A29-4656-A899-E3F39069C47E}"); To("{148714C8-FE0D-4553-B034-439D91468761}"); + To("{23BA95A4-FCCE-49B0-8AA1-45312B103A9B}"); + To("{7DDCE198-9CA4-430C-8BBC-A66D80CA209F}"); + To("{F74CDA0C-7AAA-48C8-94C6-C6EC3C06F599}"); // To 14.0.0 To("{419827A0-4FCE-464B-A8F3-247C6092AF55}"); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhookDatabaseLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhookDatabaseLock.cs new file mode 100644 index 0000000000..f611e82767 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhookDatabaseLock.cs @@ -0,0 +1,30 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; + +public class AddWebhookDatabaseLock : MigrationBase +{ + public AddWebhookDatabaseLock(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + Sql sql = Database.SqlContext.Sql() + .Select() + .From() + .Where(x => x.Id == Constants.Locks.WebhookLogs); + + LockDto? webhookLogsLock = Database.FirstOrDefault(sql); + + if (webhookLogsLock is null) + { + Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.WebhookLogs, Name = "WebhookLogs" }); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/ChangeLogStatusCode.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/ChangeLogStatusCode.cs new file mode 100644 index 0000000000..2df244be8c --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/ChangeLogStatusCode.cs @@ -0,0 +1,45 @@ +using System.Net; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; + +public class ChangeLogStatusCode : MigrationBase +{ + public ChangeLogStatusCode(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + if (!TableExists(Constants.DatabaseSchema.Tables.WebhookLog)) + { + return; + } + + Sql fetchQuery = Database.SqlContext.Sql() + .Select() + .From(); + + // Use a custom SQL query to prevent selecting explicit columns (sortOrder doesn't exist yet) + List webhookLogDtos = Database.Fetch(fetchQuery); + + Sql deleteQuery = Database.SqlContext.Sql() + .Delete(); + + Database.Execute(deleteQuery); + + foreach (WebhookLogDto webhookLogDto in webhookLogDtos) + { + if (Enum.TryParse(webhookLogDto.StatusCode, out HttpStatusCode statusCode)) + { + webhookLogDto.StatusCode = $"{statusCode.ToString()} ({(int)statusCode})"; + } + } + + Database.InsertBatch(webhookLogDtos); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/ChangeWebhookRequestObjectColumnToNvarcharMax.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/ChangeWebhookRequestObjectColumnToNvarcharMax.cs new file mode 100644 index 0000000000..8b0e07a63c --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/ChangeWebhookRequestObjectColumnToNvarcharMax.cs @@ -0,0 +1,88 @@ +using System.Linq.Expressions; +using System.Text; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; + +public class ChangeWebhookRequestObjectColumnToNvarcharMax : MigrationBase +{ + public ChangeWebhookRequestObjectColumnToNvarcharMax(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + // We don't need to run this migration for SQLite, since ntext is not a thing there, text is just text. + if (DatabaseType == DatabaseType.SQLite) + { + return; + } + + MigrateNtextColumn("requestObject", Constants.DatabaseSchema.Tables.WebhookRequest, x => x.RequestObject); + } + + private void MigrateNtextColumn(string columnName, string tableName, Expression> fieldSelector, bool nullable = true) + { + var columnType = ColumnType(tableName, columnName); + if (columnType is null || columnType.Equals("ntext", StringComparison.InvariantCultureIgnoreCase) is false) + { + return; + } + + var oldColumnName = $"Old{columnName}"; + + // Rename the column so we can create the new one and copy over the data. + Rename + .Column(columnName) + .OnTable(tableName) + .To(oldColumnName) + .Do(); + + // Create new column with the correct type + // This is pretty ugly, but we have to do ti this way because the CacheInstruction.Instruction column doesn't support nullable. + // So we have to populate with some temporary placeholder value before we copy over the actual data. + ICreateColumnOptionBuilder builder = Create + .Column(columnName) + .OnTable(tableName) + .AsCustom("nvarchar(max)"); + + if (nullable is false) + { + builder + .NotNullable() + .WithDefaultValue("Placeholder"); + } + else + { + builder.Nullable(); + } + + builder.Do(); + + // Copy over data NPOCO doesn't support this for some reason, so we'll have to do it like so + // While we're add it we'll also set all the old values to be NULL since it's recommended here: + // https://learn.microsoft.com/en-us/sql/t-sql/data-types/ntext-text-and-image-transact-sql?view=sql-server-ver16#remarks + StringBuilder queryBuilder = new StringBuilder() + .AppendLine($"UPDATE {tableName}") + .AppendLine("SET") + .Append($"\t{SqlSyntax.GetFieldNameForUpdate(fieldSelector)} = {SqlSyntax.GetQuotedTableName(tableName)}.{SqlSyntax.GetQuotedColumnName(oldColumnName)}"); + + if (nullable) + { + queryBuilder.AppendLine($"\n,\t{SqlSyntax.GetQuotedColumnName(oldColumnName)} = NULL"); + } + + Sql copyDataQuery = Database.SqlContext.Sql(queryBuilder.ToString()); + Database.Execute(copyDataQuery); + + // Delete old column + Delete + .Column(oldColumnName) + .FromTable(tableName) + .Do(); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookRequestDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookRequestDto.cs index e1f55ad042..80739770a0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookRequestDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookRequestDto.cs @@ -20,6 +20,7 @@ public class WebhookRequestDto public string Alias { get; set; } = string.Empty; [Column(Name = "requestObject")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] [NullSetting(NullSetting = NullSettings.Null)] public string? RequestObject { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index 9e6328501f..ad081b5bda 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -21,7 +21,7 @@ internal static class WebhookFactory return entity; } - public static WebhookDto BuildDto(Webhook webhook) + public static WebhookDto BuildDto(IWebhook webhook) { var dto = new WebhookDto { @@ -34,21 +34,21 @@ internal static class WebhookFactory return dto; } - public static IEnumerable BuildEntityKey2WebhookDto(Webhook webhook) => + public static IEnumerable BuildEntityKey2WebhookDto(IWebhook webhook) => webhook.ContentTypeKeys.Select(x => new Webhook2ContentTypeKeysDto { ContentTypeKey = x, WebhookId = webhook.Id, }); - public static IEnumerable BuildEvent2WebhookDto(Webhook webhook) => + public static IEnumerable BuildEvent2WebhookDto(IWebhook webhook) => webhook.Events.Select(x => new Webhook2EventsDto { Event = x, WebhookId = webhook.Id, }); - public static IEnumerable BuildHeaders2WebhookDtos(Webhook webhook) => + public static IEnumerable BuildHeaders2WebhookDtos(IWebhook webhook) => webhook.Headers.Select(x => new Webhook2HeadersDto { Key = x.Key, diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs index 910f1178d4..af6c561d90 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs @@ -2,7 +2,6 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; @@ -14,26 +13,39 @@ public class WebhookLogRepository : IWebhookLogRepository { private readonly IScopeAccessor _scopeAccessor; + private IUmbracoDatabase Database + { + get + { + if (_scopeAccessor.AmbientScope is null) + { + throw new NotSupportedException("Need to be executed in a scope"); + } + + return _scopeAccessor.AmbientScope.Database; + } + } + public WebhookLogRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; public async Task CreateAsync(WebhookLog log) { WebhookLogDto dto = WebhookLogFactory.CreateDto(log); - var result = await _scopeAccessor.AmbientScope?.Database.InsertAsync(dto)!; + var result = await Database.InsertAsync(dto)!; var id = Convert.ToInt32(result); log.Id = id; } public async Task> GetPagedAsync(int skip, int take) { - Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + Sql sql = Database.SqlContext.Sql() .Select() .From() .OrderByDescending(x => x.Date); PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); - Page? page = await _scopeAccessor.AmbientScope?.Database.PageAsync(pageNumber + 1, pageSize, sql)!; + Page? page = await Database.PageAsync(pageNumber + 1, pageSize, sql)!; return new PagedModel { @@ -41,4 +53,25 @@ public class WebhookLogRepository : IWebhookLogRepository Items = page.Items.Select(WebhookLogFactory.DtoToEntity), }; } + + public async Task> GetOlderThanDate(DateTime date) + { + Sql sql = Database.SqlContext.Sql() + .Select() + .From() + .Where(log => log.Date < date); + + List? logs = await Database.FetchAsync(sql); + + return logs.Select(WebhookLogFactory.DtoToEntity); + } + + public async Task DeleteByIds(int[] ids) + { + Sql query = Database.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, ids); + + await Database.ExecuteAsync(query); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index b8fe5e52fe..7358374f3b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -14,7 +14,7 @@ public class WebhookRepository : IWebhookRepository public WebhookRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; - public async Task> GetAllAsync(int skip, int take) + public async Task> GetAllAsync(int skip, int take) { Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .Select() @@ -22,15 +22,16 @@ public class WebhookRepository : IWebhookRepository List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; - return new PagedModel + return new PagedModel { Items = await DtosToEntities(webhookDtos.Skip(skip).Take(take)), Total = webhookDtos.Count, }; } - public async Task CreateAsync(Webhook webhook) + public async Task CreateAsync(IWebhook webhook) { + webhook.AddingEntity(); WebhookDto webhookDto = WebhookFactory.BuildDto(webhook); var result = await _scopeAccessor.AmbientScope?.Database.InsertAsync(webhookDto)!; @@ -45,7 +46,7 @@ public class WebhookRepository : IWebhookRepository return webhook; } - public async Task GetAsync(Guid key) + public async Task GetAsync(Guid key) { Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .Select() @@ -57,7 +58,7 @@ public class WebhookRepository : IWebhookRepository return webhookDto is null ? null : await DtoToEntity(webhookDto); } - public async Task> GetByAliasAsync(string alias) + public async Task> GetByAliasAsync(string alias) { Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .SelectAll() @@ -68,14 +69,14 @@ public class WebhookRepository : IWebhookRepository List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; - return new PagedModel + return new PagedModel { Items = await DtosToEntities(webhookDtos), Total = webhookDtos.Count, }; } - public async Task DeleteAsync(Webhook webhook) + public async Task DeleteAsync(IWebhook webhook) { Sql sql = _scopeAccessor.AmbientScope!.Database.SqlContext.Sql() .Delete() @@ -84,8 +85,9 @@ public class WebhookRepository : IWebhookRepository await _scopeAccessor.AmbientScope?.Database.ExecuteAsync(sql)!; } - public async Task UpdateAsync(Webhook webhook) + public async Task UpdateAsync(IWebhook webhook) { + webhook.UpdatingEntity(); WebhookDto dto = WebhookFactory.BuildDto(webhook); await _scopeAccessor.AmbientScope?.Database.UpdateAsync(dto)!; @@ -101,7 +103,7 @@ public class WebhookRepository : IWebhookRepository _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); } - private void InsertManyToOneReferences(Webhook webhook) + private void InsertManyToOneReferences(IWebhook webhook) { IEnumerable buildEntityKey2WebhookDtos = WebhookFactory.BuildEntityKey2WebhookDto(webhook); IEnumerable buildEvent2WebhookDtos = WebhookFactory.BuildEvent2WebhookDto(webhook); @@ -112,9 +114,9 @@ public class WebhookRepository : IWebhookRepository _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(header2WebhookDtos); } - private async Task> DtosToEntities(IEnumerable dtos) + private async Task> DtosToEntities(IEnumerable dtos) { - List result = new(); + List result = new(); foreach (WebhookDto webhook in dtos) { @@ -124,7 +126,7 @@ public class WebhookRepository : IWebhookRepository return result; } - private async Task DtoToEntity(WebhookDto dto) + private async Task DtoToEntity(WebhookDto dto) { List? webhookEntityKeyDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync("WHERE webhookId = @webhookId", new { webhookId = dto.Id })!; List? event2WebhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync("WHERE webhookId = @webhookId", new { webhookId = dto.Id })!; diff --git a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs index b107d82999..cbaf6549f0 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs @@ -9,7 +9,7 @@ public class WebhookFiringService : IWebhookFiringService public WebhookFiringService(IWebhookRequestService webhookRequestService) => _webhookRequestService = webhookRequestService; - public async Task FireAsync(Webhook webhook, string eventAlias, object? payload, CancellationToken cancellationToken) => + public async Task FireAsync(IWebhook webhook, string eventAlias, object? payload, CancellationToken cancellationToken) => await _webhookRequestService.CreateAsync(webhook.Key, eventAlias, payload); } diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 75c652b30c..33b7bd36f4 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -11,36 +11,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs index fad8722b77..f38b9dd2fc 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs @@ -736,16 +736,22 @@ public class ContentStore { EnsureLocked(); - IPublishedContentType?[] contentTypes = _contentTypesById + IPublishedContentType[] contentTypes = _contentTypesById .Where(kvp => kvp.Value.Value != null && kvp.Value.Value.PropertyTypes.Any(p => dataTypeIds.Contains(p.DataType.Id))) .Select(kvp => kvp.Value.Value) .Select(x => getContentType(x!.Id)) - .Where(x => x != null) // poof, gone, very unlikely and probably an anomaly + .WhereNotNull() // poof, gone, very unlikely and probably an anomaly .ToArray(); - var contentTypeIdsA = contentTypes.Select(x => x!.Id).ToArray(); + // all content types that are affected by this data type update must be updated + foreach (IPublishedContentType contentType in contentTypes) + { + SetContentTypeLocked(contentType); + } + + var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); var contentTypeNodes = new Dictionary>(); foreach (var id in contentTypeIdsA) { @@ -761,7 +767,7 @@ public class ContentStore } } - foreach (IPublishedContentType contentType in contentTypes.WhereNotNull()) + foreach (IPublishedContentType contentType in contentTypes) { // again, weird situation if (contentTypeNodes.ContainsKey(contentType.Id) == false) diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index a7f8c42823..6ab806c8df 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -189,6 +189,9 @@ internal class PublishedSnapshotService : IPublishedSnapshotService } } + // Ensure all published data types are updated + _publishedContentTypeFactory.NotifyDataTypeChanges(); + Notify(_contentStore, payloads, RefreshContentTypesLocked); Notify(_mediaStore, payloads, RefreshMediaTypesLocked); diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index a61eba6c79..75dfa13ec6 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 14a5eb8d08..8d523ecd4f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -2317,8 +2317,10 @@ public class ContentController : ContentControllerBase } // Validate permissions on node - EntityPermission? permission = _userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, node.Path); - if (permission?.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false) + var permissions = _userService.GetAllPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, node.Path); + + if (permissions.Any(x => + x.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) && x.EntityId == node.Id) == false) { HttpContext.SetReasonPhrase("Permission Denied."); return BadRequest("You do not have permission to assign domains on that node."); diff --git a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs index a5e5e3b447..11e8099a57 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs @@ -20,7 +20,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers; -[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] +[Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class RedirectUrlManagementController : UmbracoAuthorizedApiController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs index 336237dc07..ac230ccd4c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs @@ -37,7 +37,7 @@ public class WebhookController : UmbracoAuthorizedJsonController [HttpGet] public async Task GetAll(int skip = 0, int take = int.MaxValue) { - PagedModel webhooks = await _webhookService.GetAllAsync(skip, take); + PagedModel webhooks = await _webhookService.GetAllAsync(skip, take); IEnumerable webhookViewModels = webhooks.Items.Select(_webhookPresentationFactory.Create); @@ -47,24 +47,24 @@ public class WebhookController : UmbracoAuthorizedJsonController [HttpPut] public async Task Update(WebhookViewModel webhookViewModel) { - Webhook webhook = _umbracoMapper.Map(webhookViewModel)!; + IWebhook webhook = _umbracoMapper.Map(webhookViewModel)!; - Attempt result = await _webhookService.UpdateAsync(webhook); + Attempt result = await _webhookService.UpdateAsync(webhook); return result.Success ? Ok(_webhookPresentationFactory.Create(webhook)) : WebhookOperationStatusResult(result.Status); } [HttpPost] public async Task Create(WebhookViewModel webhookViewModel) { - Webhook webhook = _umbracoMapper.Map(webhookViewModel)!; - Attempt result = await _webhookService.CreateAsync(webhook); + IWebhook webhook = _umbracoMapper.Map(webhookViewModel)!; + Attempt result = await _webhookService.CreateAsync(webhook); return result.Success ? Ok(_webhookPresentationFactory.Create(webhook)) : WebhookOperationStatusResult(result.Status); } [HttpGet] public async Task GetByKey(Guid key) { - Webhook? webhook = await _webhookService.GetAsync(key); + IWebhook? webhook = await _webhookService.GetAsync(key); return webhook is null ? NotFound() : Ok(_webhookPresentationFactory.Create(webhook)); } @@ -72,7 +72,7 @@ public class WebhookController : UmbracoAuthorizedJsonController [HttpDelete] public async Task Delete(Guid key) { - Attempt result = await _webhookService.DeleteAsync(key); + Attempt result = await _webhookService.DeleteAsync(key); return result.Success ? Ok() : WebhookOperationStatusResult(result.Status); } diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index 8d9982d0c6..197a06278c 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -9,13 +9,13 @@ public class WebhookMapDefinition : IMapDefinition { public void DefineMaps(IUmbracoMapper mapper) { - mapper.Define((_, _) => new Webhook(string.Empty), Map); + mapper.Define((_, _) => new Webhook(string.Empty), Map); mapper.Define((_, _) => new WebhookEventViewModel(), Map); mapper.Define((_, _) => new WebhookLogViewModel(), Map); } // Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -Key -UpdateDate - private void Map(WebhookViewModel source, Webhook target, MapperContext context) + private void Map(WebhookViewModel source, IWebhook target, MapperContext context) { target.ContentTypeKeys = source.ContentTypeKeys; target.Events = source.Events.Select(x => x.Alias).ToArray(); diff --git a/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs b/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs index 5d94607998..77c2104a8c 100644 --- a/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs +++ b/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs @@ -6,5 +6,5 @@ namespace Umbraco.Cms.Web.BackOffice.Services; [Obsolete("Will be moved to a new namespace in V14")] public interface IWebhookPresentationFactory { - WebhookViewModel Create(Webhook webhook); + WebhookViewModel Create(IWebhook webhook); } diff --git a/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs b/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs index ef4e052596..bf33716c08 100644 --- a/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs +++ b/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs @@ -11,7 +11,7 @@ internal class WebhookPresentationFactory : IWebhookPresentationFactory public WebhookPresentationFactory(WebhookEventCollection webhookEventCollection) => _webhookEventCollection = webhookEventCollection; - public WebhookViewModel Create(Webhook webhook) + public WebhookViewModel Create(IWebhook webhook) { var target = new WebhookViewModel { diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index a64d0d2408..8a9f3f8033 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs index 1813c75932..ad5f480a72 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs @@ -27,7 +27,7 @@ internal class AspNetCoreSessionManager : ISessionIdResolver, ISessionManager /// /// If session isn't enabled this will throw an exception so we check /// - private bool IsSessionsAvailable => !(_httpContextAccessor.HttpContext?.Features.Get() is null); + private bool IsSessionsAvailable => !(_httpContextAccessor.HttpContext?.Features.Get()?.Session is null); public string? GetSessionValue(string key) { diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 0f2a97899e..f59c00eebf 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -220,6 +220,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddRecurringBackgroundJob(); builder.Services.AddRecurringBackgroundJob(); builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); builder.Services.AddRecurringBackgroundJob(provider => new ReportSiteJob( provider.GetRequiredService>(), diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 5688c37c5b..c7e259d283 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -7,15 +7,19 @@ - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index ef62c3310c..1db798245a 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -9,7 +9,7 @@ "@microsoft/signalr": "7.0.12", "@umbraco-ui/uui": "1.5.0", "@umbraco-ui/uui-css": "1.5.0", - "ace-builds": "1.31.0", + "ace-builds": "1.31.1", "angular": "1.8.3", "angular-animate": "1.8.3", "angular-aria": "1.8.3", @@ -3116,9 +3116,9 @@ } }, "node_modules/ace-builds": { - "version": "1.31.0", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.31.0.tgz", - "integrity": "sha512-nitIhcUYA6wyO3lo2WZBPX5fcjllW6XFt4EFyHwcN2Fp70/IZwz8tdw6a0+8udDEwDj/ebt3aWEClIyCs/6qYA==" + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.31.1.tgz", + "integrity": "sha512-3DnE5bZF6Ji+l4F5acoLk+rV7mxrUt1C4r61Xy9owp5rVM4lj5NL8GJfoX6Jnnbhx6kKV7Vdpb+Tco+0ORTvhg==" }, "node_modules/acorn": { "version": "8.9.0", @@ -20332,9 +20332,9 @@ } }, "ace-builds": { - "version": "1.31.0", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.31.0.tgz", - "integrity": "sha512-nitIhcUYA6wyO3lo2WZBPX5fcjllW6XFt4EFyHwcN2Fp70/IZwz8tdw6a0+8udDEwDj/ebt3aWEClIyCs/6qYA==" + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.31.1.tgz", + "integrity": "sha512-3DnE5bZF6Ji+l4F5acoLk+rV7mxrUt1C4r61Xy9owp5rVM4lj5NL8GJfoX6Jnnbhx6kKV7Vdpb+Tco+0ORTvhg==" }, "acorn": { "version": "8.9.0", @@ -30871,9 +30871,9 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "tinymce": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.7.1.tgz", - "integrity": "sha512-SIGJgWk2d/X59VbO+i81QfNx2EP1P5t+sza2/1So3OLGtmMBhEJMag7sN/Mo8sq4s0niwb65Z51yLju32jP11g==" + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.7.3.tgz", + "integrity": "sha512-J7WmYIi/gt1RvZ6Ap2oQiUjzAoiS9pfV+d4GnKuZuPu8agmlAEAInNmMvMjfCNBzHv4JnZXY7qlHUAI0IuYQVA==" }, "to-absolute-glob": { "version": "2.0.2", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 315425bc77..3d3dc9da15 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -21,7 +21,7 @@ "@microsoft/signalr": "7.0.12", "@umbraco-ui/uui": "1.5.0", "@umbraco-ui/uui-css": "1.5.0", - "ace-builds": "1.31.0", + "ace-builds": "1.31.1", "angular": "1.8.3", "angular-animate": "1.8.3", "angular-aria": "1.8.3", diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js index 93bbd842b0..3a545de2e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js @@ -52,7 +52,7 @@ promises.push(loadEvents()); if (!$routeParams.create) { - + promises.push(webhooksResource.getByKey($routeParams.id).then(webhook => { vm.webhook = webhook; @@ -60,7 +60,7 @@ const eventType = vm.webhook ? vm.webhook.events[0].eventType.toLowerCase() : null; const contentTypes = webhook.contentTypeKeys.map(x => ({ key: x })); - + getEntities(contentTypes, eventType); makeBreadcrumbs(); @@ -150,14 +150,37 @@ default: return; } - + vm.contentTypes = []; selection.forEach(entity => { resource.getById(entity.key) .then(data => { if (!vm.contentTypes.some(x => x.key === data.key)) { vm.contentTypes.push(data); } - }); + }).catch(err => { + let name; + switch (eventType.toCamelCase()) { + case "content": + name = "Unknown content type"; + break; + case "media": + name = "Unknown media type"; + break; + case "member": + name = "Unknown member type"; + break; + default: + name = "Unknown type"; + } + + let data = { + icon: "icon-alert", + name: name, + description: "An error occurred while loading the content type.", + key: entity.key + } + vm.contentTypes.push(data); + }); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js index c6b45530d8..c4d032b806 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js @@ -55,7 +55,7 @@ } function isChecked(log) { - return log.statusCode === "OK"; + return log.statusCode === "OK (200)"; } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html index 9eec01ce05..9b1a59a0db 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html @@ -16,8 +16,9 @@ + size="m"> + {{ log.webhookKey }} {{ log.formattedLogDate }} diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html index d1ce18477e..4e60ed2d63 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html @@ -15,17 +15,17 @@ - +
- - + +
-
{{model.webhookLogEntry.response.statusDescription}} ({{model.webhookLogEntry.response.statusCode}})
+
{{model.log.statusCode}}
- +
{{model.log.formattedLogDate}}
@@ -34,10 +34,6 @@
{{model.log.url}}
- -
{{model.log.statusCode}}
-
-
{{model.log.eventAlias}}
diff --git a/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts b/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts index 44ebab1b3c..4d9f972743 100644 --- a/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts +++ b/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts @@ -8,15 +8,23 @@ import { when } from 'lit/directives/when.js'; * @element umb-auth-layout * @slot - The content of the layout * @cssprop --umb-login-background - The background of the layout (default: #f4f4f4) + * @cssprop --umb-login-primary-color - The color of the headline (default: #283a97) + * @cssprop --umb-login-text-color - The color of the text (default: #000) + * @cssprop --umb-login-header-font-size - The font-size of the headline (default: 3rem) + * @cssprop --umb-login-header-font-size-large - The font-size of the headline on large screens (default: 4rem) + * @cssprop --umb-login-header-secondary-font-size - The font-size of the secondary headline (default: 2.4rem) * @cssprop --umb-login-image - The background of the image wrapper (default: the value of the backgroundImage property) * @cssprop --umb-login-image-display - The display of the image wrapper (default: flex) + * @cssprop --umb-login-image-border-radius - The border-radius of the image wrapper (default: 38px) * @cssprop --umb-login-content-background - The background of the content wrapper (default: none) * @cssprop --umb-login-content-display - The display of the content wrapper (default: flex) * @cssprop --umb-login-content-width - The width of the content wrapper (default: 100%) * @cssprop --umb-login-content-height - The height of the content wrapper (default: 100%) + * @cssprop --umb-login-content-border-radius - The border-radius of the content wrapper (default: 0) * @cssprop --umb-login-align-items - The align-items of the main wrapper (default: unset) - * @cssprop --umb-curves-color - The color of the curves (default: #f5c1bc) - * @cssprop --umb-curves-display - The display of the curves (default: inline) + * @cssprop --umb-login-button-border-radius - The border-radius of the buttons (default: 45px) + * @cssprop --umb-login-curves-color - The color of the curves (default: #f5c1bc) + * @cssprop --umb-login-curves-display - The display of the curves (default: inline) */ @customElement('umb-auth-layout') export class UmbAuthLayoutElement extends LitElement { @@ -97,20 +105,21 @@ export class UmbAuthLayoutElement extends LitElement { static styles: CSSResultGroup = [ css` :host { - --uui-color-interactive: #283a97; - --uui-button-border-radius: 45px; + --uui-color-interactive: var(--umb-login-primary-color, #283a97); + --uui-button-border-radius: var(--umb-login-button-border-radius, 45px); --uui-color-default: var(--uui-color-interactive); --uui-button-height: 42px; --uui-select-height: 38px; --input-height: 40px; - --header-font-size: 3rem; - --header-secondary-font-size: 2.4rem; - --curves-color: var(--umb-curves-color, #f5c1bc); - --curves-display: var(--umb-curves-display, inline); + --header-font-size: var(--umb-login-header-font-size, 3rem); + --header-secondary-font-size: var(--umb-login-header-secondary-font-size, 2.4rem); + --curves-color: var(--umb-login-curves-color, #f5c1bc); + --curves-display: var(--umb-login-curves-display, inline); display: block; background: var(--umb-login-background, #f4f4f4); + color: var(--umb-login-text-color, #000); } #main-no-image, @@ -136,6 +145,7 @@ export class UmbAuthLayoutElement extends LitElement { height: var(--umb-login-content-height, 100%); box-sizing: border-box; overflow: auto; + border-radius: var(--umb-login-content-border-radius, 0); } #content { @@ -148,7 +158,7 @@ export class UmbAuthLayoutElement extends LitElement { background: var(--umb-login-image, var(--image)); width: 100%; height: 100%; - border-radius: 38px; + border-radius: var(--umb-login-image-border-radius, 38px); position: relative; overflow: hidden; color: var(--curves-color); @@ -181,7 +191,7 @@ export class UmbAuthLayoutElement extends LitElement { @media only screen and (min-width: 900px) { :host { - --header-font-size: 4rem; + --header-font-size: var(--umb-login-header-font-size-large, 4rem); } #main { diff --git a/src/Umbraco.Web.UI.New/Umbraco.Web.UI.New.csproj b/src/Umbraco.Web.UI.New/Umbraco.Web.UI.New.csproj index c61d74a1ec..313dd1ec68 100644 --- a/src/Umbraco.Web.UI.New/Umbraco.Web.UI.New.csproj +++ b/src/Umbraco.Web.UI.New/Umbraco.Web.UI.New.csproj @@ -2,6 +2,7 @@ Umbraco.Cms.Web.UI.New false + false diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index afb8879f64..257f4d5a72 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -3,6 +3,7 @@ Umbraco.Cms.Web.UI false false + false diff --git a/src/Umbraco.Web.UI/umbraco/UmbracoBackOffice/Default.cshtml b/src/Umbraco.Web.UI/umbraco/UmbracoBackOffice/Default.cshtml index 7e66a42d39..29c3e95245 100644 --- a/src/Umbraco.Web.UI/umbraco/UmbracoBackOffice/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/UmbracoBackOffice/Default.cshtml @@ -45,13 +45,9 @@