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 @@ - - - - - - - Umbraco + + + Umbraco For full functionality of Umbraco CMS it is necessary to enable JavaScript. Here are the instructions how to enable JavaScript in your web browser. @@ -113,6 +109,7 @@ diff --git a/src/Umbraco.Web.UI/umbraco/UmbracoInstall/Index.cshtml b/src/Umbraco.Web.UI/umbraco/UmbracoInstall/Index.cshtml index 68049c6c91..a8cb03c00f 100644 --- a/src/Umbraco.Web.UI/umbraco/UmbracoInstall/Index.cshtml +++ b/src/Umbraco.Web.UI/umbraco/UmbracoInstall/Index.cshtml @@ -16,7 +16,7 @@ - + ContentSettings -@inject IOptions SecuritySettings -@inject IEmailSender EmailSender -@inject IHostingEnvironment HostingEnvironment -@inject IOptions GlobalSettings -@inject IBackOfficeExternalLoginProviders ExternalLogins -@inject LinkGenerator LinkGenerator @{ - var backOfficePath = GlobalSettings.Value.GetBackOfficePath(HostingEnvironment); - var loginLogoImage = ContentSettings.Value.LoginLogoImage; - var loginBackgroundImage = ContentSettings.Value.LoginBackgroundImage; - var usernameIsEmail = SecuritySettings.Value.UsernameIsEmail; - var allowUserInvite = EmailSender.CanSendRequiredEmail(); - var allowPasswordReset = SecuritySettings.Value.AllowPasswordReset && EmailSender.CanSendRequiredEmail(); - var disableLocalLogin = ExternalLogins.HasDenyLocalLogin(); - var externalLoginsUrl = LinkGenerator.GetPathByAction(nameof(BackOfficeController.ExternalLogin), ControllerExtensions.GetControllerName(), new { area = Constants.Web.Mvc.BackOfficeArea }); - var externalLoginProviders = await ExternalLogins.GetBackOfficeProvidersAsync(); + Layout = null; } -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + - - - - - - - - Umbraco - - - - - + Install Umbraco + + + - - - - @foreach (var provider in externalLoginProviders) - { - - - } - - - + + + + + + + + + + + + + + A server error occurred + This is most likely due to an error during application startup + + + + + + + + + + + Did you know + + + + {{installer.feedback}} + + + There has been a problem with the build. + This might be because you could be offline or on a slow connection. Please try the following steps + + Make sure you have Node.js installed. + Open command prompt and cd to \src\Umbraco.Web.UI.Client. + Check to see if \src\Umbraco.Web.UI.Client\node_modules folder exists (this could be hidden); if so, delete it. + Run npm ci; if successfull the node_modules folder should be created in the Umbraco.Web.UI.Client directory. + Run \build\build.ps1. + + + + + + + diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 452095c8db..681ff78c7e 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -10,6 +10,7 @@ true . NU5128 + false diff --git a/templates/UmbracoPackage/UmbracoPackage.csproj b/templates/UmbracoPackage/UmbracoPackage.csproj index 268611b9ae..98f5bac3ad 100644 --- a/templates/UmbracoPackage/UmbracoPackage.csproj +++ b/templates/UmbracoPackage/UmbracoPackage.csproj @@ -8,6 +8,7 @@ ... umbraco plugin package UmbracoPackage + false diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index 4e51364079..1c530223ad 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -4,6 +4,7 @@ enable enable Umbraco.Cms.Web.UI + false @@ -12,8 +13,8 @@ - - + + diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props new file mode 100644 index 0000000000..41398eed6e --- /dev/null +++ b/tests/Directory.Packages.props @@ -0,0 +1,24 @@ + + + + true + NU1507 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Umbraco.TestData/Umbraco.TestData.csproj b/tests/Umbraco.TestData/Umbraco.TestData.csproj index c118664df2..7e19740a31 100644 --- a/tests/Umbraco.TestData/Umbraco.TestData.csproj +++ b/tests/Umbraco.TestData/Umbraco.TestData.csproj @@ -1,6 +1,6 @@ - + diff --git a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 885fc01b0e..09a4600005 100644 --- a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -1,12 +1,16 @@ Exe + false + false + false + false - - - + + + diff --git a/tests/Umbraco.Tests.Common/Builders/WebhookBuilder.cs b/tests/Umbraco.Tests.Common/Builders/WebhookBuilder.cs new file mode 100644 index 0000000000..96c76fd74c --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/WebhookBuilder.cs @@ -0,0 +1,80 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class WebhookBuilder + : BuilderBase, + IWithIdBuilder, + IWithKeyBuilder +{ + private int? _id; + private Guid? _key; + private string? _url; + private bool? _enabled; + private Guid[] _entityKeys; + private string[]? _events; + private Dictionary? _headers; + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + public WebhookBuilder WithUrl(string url) + { + _url = url; + return this; + } + + public WebhookBuilder WithEnabled(bool enabled) + { + _enabled = enabled; + return this; + } + + public WebhookBuilder WithEntityKeys(Guid[] entityKeys) + { + _entityKeys = entityKeys; + return this; + } + + public WebhookBuilder WithEvents(string[] events) + { + _events = events; + return this; + } + + public WebhookBuilder WithHeaders(Dictionary headers) + { + _headers = headers; + return this; + } + + public override Webhook Build() + { + var id = _id ?? 1; + var key = _key ?? Guid.NewGuid(); + var url = _url ?? "https://example.com"; + var enabled = _enabled ?? true; + var entityKeys = _entityKeys; + var events = _events; + var headers = _headers; + + return new Webhook(url, enabled, entityKeys, events, headers) + { + Id = id, + Key = key + }; + } +} diff --git a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index 8dd0bd4060..bf0dc9ddc1 100644 --- a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -9,11 +9,10 @@ - - - - - + + + + diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 91f40cd221..02d1e272a8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -10,11 +10,11 @@ - - - - - + + + + + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs index cdda6f02f5..1fc5501073 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs @@ -2,8 +2,11 @@ // See LICENSE for more details. using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions; @@ -49,4 +52,19 @@ public class UdiGetterExtensionsTests var result = partialView.GetUdi(); Assert.AreEqual(expected, result.ToString()); } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://webhook/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForWebhook(string key, string expected) + { + var builder = new WebhookBuilder(); + var webhook = builder + .WithKey(Guid.Parse(key)) + .Build(); + + Udi result = webhook.GetUdi(); + Assert.AreEqual(expected, result.ToString()); + + result = ((IEntity)webhook).GetUdi(); + Assert.AreEqual(expected, result.ToString()); + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs index b0fc8cbb43..6233e22041 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using AutoFixture.NUnit3; using Microsoft.Extensions.Options; using Moq; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/WebhookBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/WebhookBuilderTests.cs new file mode 100644 index 0000000000..9c8131c9f7 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/WebhookBuilderTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using NUnit.Framework; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders; + +[TestFixture] +public class WebhookBuilderTests +{ + [Test] + public void Is_Built_Correctly() + { + // Arrange + const int id = 1; + var key = Guid.NewGuid(); + const string url = "https://www.test.com"; + const bool enabled = true; + Guid[] entityKeys = new Guid[] { Guid.NewGuid() }; + string[] events = new string[] { "ContentPublished" }; + var headers = new Dictionary() { { "Content-Type", "application/json" } }; + + var builder = new WebhookBuilder(); + + // Act + var webhook = builder + .WithId(id) + .WithKey(key) + .WithUrl(url) + .WithEnabled(enabled) + .WithEntityKeys(entityKeys) + .WithEvents(events) + .WithHeaders(headers) + .Build(); + + // Assert + Assert.AreEqual(id, webhook.Id); + Assert.AreEqual(key, webhook.Key); + Assert.AreEqual(url, webhook.Url); + Assert.AreEqual(enabled, webhook.Enabled); + Assert.AreEqual(entityKeys.Length, webhook.ContentTypeKeys.Length); + Assert.AreEqual(entityKeys[0], webhook.ContentTypeKeys[0]); + Assert.AreEqual(events.Length, webhook.Events.Length); + Assert.AreEqual(events[0], webhook.Events[0]); + Assert.AreEqual(events.Length, webhook.Events.Length); + Assert.AreEqual("application/json", webhook.Headers["Content-Type"]); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index dd459ef520..58a409dde2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -7,11 +7,10 @@ - - - - - + + + + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 20a7f00b4f..4538e40a29 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; using System.Data; -using System.Linq; -using System.Threading.Tasks; -using AngleSharp.Common; using AutoFixture.NUnit3; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -782,8 +778,8 @@ public class MemberControllerUnitTests for (var index = 0; index < resultValue.Properties.Count(); index++) { Assert.AreNotSame( - memberDisplay.Properties.GetItemByIndex(index), - resultValue.Properties.GetItemByIndex(index)); + memberDisplay.Properties.ElementAt(index), + resultValue.Properties.ElementAt(index)); // Assert.AreEqual(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); } diff --git a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj index c9275c6b94..8b66986898 100644 --- a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj +++ b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj @@ -3,6 +3,7 @@ Exe false false + false diff --git a/umbraco.sln b/umbraco.sln index a129cdb6ef..cc1ce3fc60 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B5BD12C1 tests\.editorconfig = tests\.editorconfig tests\codeanalysis.ruleset = tests\codeanalysis.ruleset tests\Directory.Build.props = tests\Directory.Build.props + tests\Directory.Packages.props = tests\Directory.Packages.props EndProjectSection EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}" @@ -140,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution umbraco.sln.DotSettings = umbraco.sln.DotSettings version.json = version.json global.json = global.json + src\Directory.Packages.props = src\Directory.Packages.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{20CE9C97-9314-4A19-BCF1-D12CF49B7205}"
For full functionality of Umbraco CMS it is necessary to enable JavaScript.
Here are the instructions how to enable JavaScript in your web browser.
This is most likely due to an error during application startup
This might be because you could be offline or on a slow connection. Please try the following steps