From 727f3fe168edc62e6ab999c275d0f096708ad699 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 11:42:28 +0200 Subject: [PATCH 001/102] Create webhook models --- src/Umbraco.Core/Models/Webhook.cs | 50 +++++++++++++++++++++++++ src/Umbraco.Core/Models/WebhookEvent.cs | 10 +++++ 2 files changed, 60 insertions(+) create mode 100644 src/Umbraco.Core/Models/Webhook.cs create mode 100644 src/Umbraco.Core/Models/WebhookEvent.cs diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs new file mode 100644 index 000000000000..5e106f7391c1 --- /dev/null +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -0,0 +1,50 @@ +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class Webhook : EntityBase +{ + private string _url; + private WebHookEvent _webHookEvent; + private string _entityName; + private Guid _entityKey; + + public Webhook(string url, WebHookEvent webHookEvent, string entityName, Guid entityKey) + { + _url = url; + _webHookEvent = webHookEvent; + _entityName = entityName; + _entityKey = entityKey; + } + + [DataMember] + public string Url + { + get => _url; + set => SetPropertyValueAndDetectChanges(value, ref _url!, nameof(Url)); + } + + [DataMember] + public WebHookEvent Event + { + get => _webHookEvent; + set => SetPropertyValueAndDetectChanges(value, ref _webHookEvent!, nameof(Event)); + } + + [DataMember] + public string EntityName + { + get => _entityName; + set => SetPropertyValueAndDetectChanges(value, ref _entityName!, nameof(EntityName)); + } + + [DataMember] + public Guid EntityKey + { + get => _entityKey; + set => SetPropertyValueAndDetectChanges(value, ref _entityKey, nameof(EntityKey)); + } +} diff --git a/src/Umbraco.Core/Models/WebhookEvent.cs b/src/Umbraco.Core/Models/WebhookEvent.cs new file mode 100644 index 000000000000..d832f0a4bcd8 --- /dev/null +++ b/src/Umbraco.Core/Models/WebhookEvent.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Models; + +public enum WebhookEvent +{ + ContentPublish, + ContentUnpublish, + ContentDelete, + MediaSave, + MediaDelete, +} From 3fa9d5f85282cbe68bb34a01b256bd6e71f81690 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 13:41:13 +0200 Subject: [PATCH 002/102] Define interfaces for service and repository --- src/Umbraco.Core/Models/Webhook.cs | 6 +++--- .../Persistence/Repositories/IWebhookRepository.cs | 7 +++++++ src/Umbraco.Core/Services/IWebHookService.cs | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs create mode 100644 src/Umbraco.Core/Services/IWebHookService.cs diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index 5e106f7391c1..d8485bf8b20f 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -8,11 +8,11 @@ namespace Umbraco.Cms.Core.Models; public class Webhook : EntityBase { private string _url; - private WebHookEvent _webHookEvent; + private WebhookEvent _webHookEvent; private string _entityName; private Guid _entityKey; - public Webhook(string url, WebHookEvent webHookEvent, string entityName, Guid entityKey) + public Webhook(string url, WebhookEvent webHookEvent, string entityName, Guid entityKey) { _url = url; _webHookEvent = webHookEvent; @@ -28,7 +28,7 @@ public string Url } [DataMember] - public WebHookEvent Event + public WebhookEvent Event { get => _webHookEvent; set => SetPropertyValueAndDetectChanges(value, ref _webHookEvent!, nameof(Event)); diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs new file mode 100644 index 000000000000..817f7428d189 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IWebhookRepository : IReadWriteQueryRepository +{ +} diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs new file mode 100644 index 000000000000..371ca7ee93c7 --- /dev/null +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebHookService +{ + Task CreateAsync(Webhook webhook); + + Task Delete(Webhook webhook); + + Task GetAsync(Guid key); + + Task GetMultipleAsync(IEnumerable keys); +} From c511fa375d96c8a95e8f3188885467e3c36ab0a6 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 13:57:02 +0200 Subject: [PATCH 003/102] Create Webhook dto and corresponding factory --- .../Persistence/Constants-DatabaseSchema.cs | 1 + .../Persistence/Dtos/WebhookDto.cs | 37 +++++++++++++++ .../Persistence/Factories/WebhookFactory.cs | 46 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 420f36c759fe..4999142a8c8b 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -86,6 +86,7 @@ public static class Tables public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; + public const string Webhook = TableNamePrefix + "Webhook"; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs new file mode 100644 index 000000000000..9a100d53fcd6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -0,0 +1,37 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + + +[TableName(Constants.DatabaseSchema.Tables.Webhook)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class WebhookDto +{ + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = true)] + public int Id { get; set; } + + [Column(Name = "key")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid Key { get; set; } + + [Column(Name = "url")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Url { get; set; } = string.Empty; + + [Column(Name = "event")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public WebhookEvent Event { get; set; } + + [Column(Name = "entityName")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string EntityName { get; set; } = string.Empty; + + [Column(Name = "entityKey")] + public Guid EntityKey { get; set; } +} + diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs new file mode 100644 index 000000000000..67b416f94b21 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -0,0 +1,46 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class WebhookFactory +{ + public static Webhook BuildEntity(WebhookDto dto) + { + var entity = new Webhook(dto.Url, dto.Event, dto.EntityName, dto.EntityKey); + + try + { + entity.DisableChangeTracking(); + + entity.Id = dto.Id; + entity.Key = dto.Key; + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } + finally + { + entity.EnableChangeTracking(); + } + } + + public static WebhookDto BuildDto(Webhook webhook) + { + var dto = new WebhookDto + { + Url = webhook.Url, + Event = webhook.Event, + EntityName = webhook.EntityName, + EntityKey = webhook.EntityKey, + Key = webhook.Key, + }; + if (webhook.HasIdentity) + { + dto.Id = webhook.Id; + } + + return dto; + } +} From b97abb438430a03ca2a73357a240b0d767e5dfbf Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 14:38:19 +0200 Subject: [PATCH 004/102] implement WebhookRepository.cs --- .../Implement/WebhookRepository.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs new file mode 100644 index 000000000000..c67e9d122931 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class WebhookRepository : EntityRepositoryBase, IWebhookRepository +{ + public WebhookRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) : base(scopeAccessor, appCaches, logger) + { + } + + protected override Webhook? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + + WebhookDto? dto = Database.First(sql); + return dto == null + ? null + : DtoToEntity(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(DtoToEntity); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(DtoToEntity); + } + + protected override void PersistNewItem(Webhook entity) + { + entity.AddingEntity(); + + WebhookDto dto = WebhookFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(Webhook entity) + { + entity.UpdatingEntity(); + + WebhookDto dto = WebhookFactory.BuildDto(entity); + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Webhook}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + $"DELETE FROM {Constants.DatabaseSchema.Tables.Webhook} WHERE id = @id", + }; + return list; + } + + private static Webhook DtoToEntity(WebhookDto dto) + { + Webhook entity = WebhookFactory.BuildEntity(dto); + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + + return entity; + } +} From f683405000a3c804cef479383e96e7742ca3dc88 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 14:39:42 +0200 Subject: [PATCH 005/102] Remove entity name from models, as that should be resolved in mapping instead --- src/Umbraco.Core/Models/Webhook.cs | 7 ------- src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs | 4 ---- 2 files changed, 11 deletions(-) diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index d8485bf8b20f..8c20ff92d905 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -34,13 +34,6 @@ public WebhookEvent Event set => SetPropertyValueAndDetectChanges(value, ref _webHookEvent!, nameof(Event)); } - [DataMember] - public string EntityName - { - get => _entityName; - set => SetPropertyValueAndDetectChanges(value, ref _entityName!, nameof(EntityName)); - } - [DataMember] public Guid EntityKey { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs index 9a100d53fcd6..9b84fa7925f1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -27,10 +27,6 @@ internal class WebhookDto [NullSetting(NullSetting = NullSettings.NotNull)] public WebhookEvent Event { get; set; } - [Column(Name = "entityName")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string EntityName { get; set; } = string.Empty; - [Column(Name = "entityKey")] public Guid EntityKey { get; set; } } From ccec84d65497545ba6b42954902f77a6c9d92cd2 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 14:43:00 +0200 Subject: [PATCH 006/102] Add new table to schema creator --- .../Migrations/Install/DatabaseSchemaCreator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index b84774307660..6f3bc845d50e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -82,7 +82,8 @@ public class DatabaseSchemaCreator typeof(ContentVersionCleanupPolicyDto), typeof(UserGroup2NodeDto), typeof(CreatedPackageSchemaDto), - typeof(UserGroup2LanguageDto) + typeof(UserGroup2LanguageDto), + typeof(WebhookDto), }; private readonly IUmbracoDatabase _database; From b42757033ac895e7dbe8412bbfba6b0517141970 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 14:44:52 +0200 Subject: [PATCH 007/102] Register repo for DI --- .../DependencyInjection/UmbracoBuilder.Repositories.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 3afb9fe64a9e..1d212f5d33e3 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -66,8 +66,9 @@ internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } From a2a906563f36e71a020f34b31427aab5c053690e Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 14:46:57 +0200 Subject: [PATCH 008/102] Remove more mentions of entityname --- src/Umbraco.Core/Models/Webhook.cs | 4 +--- .../Persistence/Factories/WebhookFactory.cs | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index 8c20ff92d905..b6a6fa005963 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -9,14 +9,12 @@ public class Webhook : EntityBase { private string _url; private WebhookEvent _webHookEvent; - private string _entityName; private Guid _entityKey; - public Webhook(string url, WebhookEvent webHookEvent, string entityName, Guid entityKey) + public Webhook(string url, WebhookEvent webHookEvent, Guid entityKey) { _url = url; _webHookEvent = webHookEvent; - _entityName = entityName; _entityKey = entityKey; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index 67b416f94b21..428163f76d55 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -7,7 +7,7 @@ internal static class WebhookFactory { public static Webhook BuildEntity(WebhookDto dto) { - var entity = new Webhook(dto.Url, dto.Event, dto.EntityName, dto.EntityKey); + var entity = new Webhook(dto.Url, dto.Event, dto.EntityKey); try { @@ -32,7 +32,6 @@ public static WebhookDto BuildDto(Webhook webhook) { Url = webhook.Url, Event = webhook.Event, - EntityName = webhook.EntityName, EntityKey = webhook.EntityKey, Key = webhook.Key, }; From 039c330c49ae67c52c11129bb196448513a0adde Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 15:03:38 +0200 Subject: [PATCH 009/102] Refactor repository to guids --- .../Persistence/Repositories/IWebhookRepository.cs | 2 +- .../Persistence/Dtos/WebhookDto.cs | 3 +-- .../Persistence/Factories/WebhookFactory.cs | 4 ++-- .../Repositories/Implement/WebhookRepository.cs | 10 +++++----- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs index 817f7428d189..897fc0f2c295 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs @@ -2,6 +2,6 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; -public interface IWebhookRepository : IReadWriteQueryRepository +public interface IWebhookRepository : IReadWriteQueryRepository { } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs index 9b84fa7925f1..b4cb692b5a9e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -1,6 +1,5 @@ using NPoco; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -25,7 +24,7 @@ internal class WebhookDto [Column(Name = "event")] [NullSetting(NullSetting = NullSettings.NotNull)] - public WebhookEvent Event { get; set; } + public string Event { get; set; } = string.Empty; [Column(Name = "entityKey")] public Guid EntityKey { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index 428163f76d55..c6a918e42ead 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -7,7 +7,7 @@ internal static class WebhookFactory { public static Webhook BuildEntity(WebhookDto dto) { - var entity = new Webhook(dto.Url, dto.Event, dto.EntityKey); + var entity = new Webhook(dto.Url, Enum.Parse(dto.Event), dto.EntityKey); try { @@ -31,7 +31,7 @@ public static WebhookDto BuildDto(Webhook webhook) var dto = new WebhookDto { Url = webhook.Url, - Event = webhook.Event, + Event = webhook.Event.ToString(), EntityKey = webhook.EntityKey, Key = webhook.Key, }; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index c67e9d122931..efafffc03f4c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -13,13 +13,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; -public class WebhookRepository : EntityRepositoryBase, IWebhookRepository +public class WebhookRepository : EntityRepositoryBase, IWebhookRepository { - public WebhookRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) : base(scopeAccessor, appCaches, logger) + public WebhookRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) : base(scopeAccessor, appCaches, logger) { } - protected override Webhook? PerformGet(int id) + protected override Webhook? PerformGet(Guid id) { Sql sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); @@ -30,7 +30,7 @@ public WebhookRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILog : DtoToEntity(dto); } - protected override IEnumerable PerformGetAll(params int[]? ids) + protected override IEnumerable PerformGetAll(params Guid[]? ids) { Sql sql = GetBaseQuery(false); @@ -86,7 +86,7 @@ protected override Sql GetBaseQuery(bool isCount) return sql; } - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Webhook}.id = @id"; + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Webhook}.key = @key"; protected override IEnumerable GetDeleteClauses() { From aab96de19d9ac00c089c3911d265b30d2969f46a Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 28 Aug 2023 15:05:21 +0200 Subject: [PATCH 010/102] Implement WebhookService --- .../DependencyInjection/UmbracoBuilder.cs | 1 + src/Umbraco.Core/Services/IWebHookService.cs | 6 ++-- src/Umbraco.Core/Services/WebhookService.cs | 35 +++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Core/Services/WebhookService.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index f35229e4e069..2c856b1a1691 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -326,6 +326,7 @@ private void AddCoreServices() Services.AddUnique(provider => new CultureImpactFactory(provider.GetRequiredService>())); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs index 371ca7ee93c7..cd2b743bc902 100644 --- a/src/Umbraco.Core/Services/IWebHookService.cs +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -6,9 +6,9 @@ public interface IWebHookService { Task CreateAsync(Webhook webhook); - Task Delete(Webhook webhook); + Task DeleteAsync(Webhook webhook); - Task GetAsync(Guid key); + Task GetAsync(Guid key); - Task GetMultipleAsync(IEnumerable keys); + Task> GetMultipleAsync(IEnumerable keys); } diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs new file mode 100644 index 000000000000..98ffb20e7f06 --- /dev/null +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -0,0 +1,35 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; + +namespace Umbraco.Cms.Core.Services; + +public class WebhookService : IWebHookService +{ + private readonly IWebhookRepository _webhookRepository; + + public WebhookService(IWebhookRepository webhookRepository) => _webhookRepository = webhookRepository; + + public Task CreateAsync(Webhook webhook) + { + _webhookRepository.Save(webhook); + return Task.CompletedTask; + } + + public Task DeleteAsync(Webhook webhook) + { + _webhookRepository.Delete(webhook); + return Task.CompletedTask; + } + + public Task GetAsync(Guid key) + { + Webhook? webhook = _webhookRepository.Get(key); + return Task.FromResult(webhook); + } + + public Task> GetMultipleAsync(IEnumerable keys) + { + IEnumerable webhooks = _webhookRepository.GetMany(keys.ToArray()); + return Task.FromResult(webhooks); + } +} From 54ac9c503e828e3a80e67e694f481e9e5a11a9f5 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Tue, 29 Aug 2023 08:37:48 +0200 Subject: [PATCH 011/102] Use scopes in service --- src/Umbraco.Core/Services/WebhookService.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 98ffb20e7f06..594043a7271e 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -1,35 +1,50 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services; public class WebhookService : IWebHookService { private readonly IWebhookRepository _webhookRepository; + private readonly ICoreScopeProvider _coreScopeProvider; - public WebhookService(IWebhookRepository webhookRepository) => _webhookRepository = webhookRepository; + public WebhookService(IWebhookRepository webhookRepository, ICoreScopeProvider coreScopeProvider) + { + _webhookRepository = webhookRepository; + _coreScopeProvider = coreScopeProvider; + } public Task CreateAsync(Webhook webhook) { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); _webhookRepository.Save(webhook); + scope.Complete(); + return Task.CompletedTask; } public Task DeleteAsync(Webhook webhook) { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); _webhookRepository.Delete(webhook); + scope.Complete(); return Task.CompletedTask; } public Task GetAsync(Guid key) { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); Webhook? webhook = _webhookRepository.Get(key); + scope.Complete(); return Task.FromResult(webhook); } public Task> GetMultipleAsync(IEnumerable keys) { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); IEnumerable webhooks = _webhookRepository.GetMany(keys.ToArray()); + scope.Complete(); return Task.FromResult(webhooks); } } From db6ed7cf073316e7ad8bfb5edf8430bc989167b3 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Tue, 29 Aug 2023 10:19:07 +0200 Subject: [PATCH 012/102] Start creating tests for service --- src/Umbraco.Core/Services/IWebHookService.cs | 2 +- src/Umbraco.Core/Services/WebhookService.cs | 4 +-- .../Implement/WebhookRepository.cs | 6 ++-- .../Services/WebhookServiceTests.cs | 34 +++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs index cd2b743bc902..b5736368ac69 100644 --- a/src/Umbraco.Core/Services/IWebHookService.cs +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Services; public interface IWebHookService { - Task CreateAsync(Webhook webhook); + Task CreateAsync(Webhook webhook); Task DeleteAsync(Webhook webhook); diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 594043a7271e..b0a240ef2f9a 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -15,13 +15,13 @@ public WebhookService(IWebhookRepository webhookRepository, ICoreScopeProvider c _coreScopeProvider = coreScopeProvider; } - public Task CreateAsync(Webhook webhook) + public Task CreateAsync(Webhook webhook) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); _webhookRepository.Save(webhook); scope.Complete(); - return Task.CompletedTask; + return Task.FromResult(webhook); } public Task DeleteAsync(Webhook webhook) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index efafffc03f4c..fbb673b4d7e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -19,10 +19,10 @@ public WebhookRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILog { } - protected override Webhook? PerformGet(Guid id) + protected override Webhook? PerformGet(Guid key) { Sql sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.Where(GetBaseWhereClause(), new { key }); WebhookDto? dto = Database.First(sql); return dto == null @@ -86,7 +86,7 @@ protected override Sql GetBaseQuery(bool isCount) return sql; } - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Webhook}.key = @key"; + protected override string GetBaseWhereClause() => "key = @key"; protected override IEnumerable GetDeleteClauses() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs new file mode 100644 index 000000000000..8ab926f6032e --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class WebhookServiceTests : UmbracoIntegrationTest +{ + private IWebHookService WebhookService => GetRequiredService(); + + [Test] + [TestCase("https://example.com", WebhookEvent.ContentPublish, "00000000-0000-0000-0000-010000000000")] + [TestCase("https://example.com", WebhookEvent.ContentDelete, "00000000-0000-0000-0000-000200000000")] + [TestCase("https://example.com", WebhookEvent.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] + [TestCase("https://example.com", WebhookEvent.MediaDelete, "00000000-0000-0000-0000-000004000000")] + [TestCase("https://example.com", WebhookEvent.MediaSave, "00000000-0000-0000-0000-000000500000")] + public async Task Enum_Stored_as_string(string url, WebhookEvent webhookEvent, Guid key) + { + var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, webhookEvent, key)); + var webhook = await WebhookService.GetAsync(createdWebhook.Key); + + Assert.Multiple(() => + { + Assert.IsNotNull(webhook); + Assert.AreEqual(webhookEvent, webhook.Event); + Assert.AreEqual(url, webhook.Url); + Assert.AreEqual(key, webhook.EntityKey); + }); + } +} From 11fd1e67ce0569e5aaa8fa68077128c0553c50bd Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Tue, 29 Aug 2023 12:35:37 +0200 Subject: [PATCH 013/102] Refactor delete to use Id and not entire entity --- src/Umbraco.Core/Services/IWebHookService.cs | 2 +- src/Umbraco.Core/Services/WebhookService.cs | 9 +++-- .../Implement/WebhookRepository.cs | 18 ++++++++-- .../Services/WebhookServiceTests.cs | 36 ++++++++++++++++++- 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs index b5736368ac69..f8f92c3dd651 100644 --- a/src/Umbraco.Core/Services/IWebHookService.cs +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -6,7 +6,7 @@ public interface IWebHookService { Task CreateAsync(Webhook webhook); - Task DeleteAsync(Webhook webhook); + Task DeleteAsync(Guid key); Task GetAsync(Guid key); diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index b0a240ef2f9a..9b31c9d04756 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -24,10 +24,15 @@ public Task CreateAsync(Webhook webhook) return Task.FromResult(webhook); } - public Task DeleteAsync(Webhook webhook) + public Task DeleteAsync(Guid key) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); - _webhookRepository.Delete(webhook); + Webhook? webhook = _webhookRepository.Get(key); + if (webhook is not null) + { + _webhookRepository.Delete(webhook); + } + scope.Complete(); return Task.CompletedTask; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index fbb673b4d7e5..9e15b1cd33c3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -24,7 +24,7 @@ public WebhookRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILog Sql sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { key }); - WebhookDto? dto = Database.First(sql); + WebhookDto? dto = Database.FirstOrDefault(sql); return dto == null ? null : DtoToEntity(dto); @@ -92,11 +92,25 @@ protected override IEnumerable GetDeleteClauses() { var list = new List { - $"DELETE FROM {Constants.DatabaseSchema.Tables.Webhook} WHERE id = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.Webhook} WHERE key = @key", }; return list; } + protected override Guid GetEntityId(Webhook entity) + => entity.Key; + + protected override void PersistDeletedItem(Webhook entity) + { + IEnumerable deletes = GetDeleteClauses(); + foreach (var delete in deletes) + { + Database.Execute(delete, new { key = GetEntityId(entity) }); + } + + entity.DeleteDate = DateTime.Now; + } + private static Webhook DtoToEntity(WebhookDto dto) { Webhook entity = WebhookFactory.BuildEntity(dto); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index 8ab926f6032e..20ec1f64dacf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -18,7 +18,7 @@ public class WebhookServiceTests : UmbracoIntegrationTest [TestCase("https://example.com", WebhookEvent.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] [TestCase("https://example.com", WebhookEvent.MediaDelete, "00000000-0000-0000-0000-000004000000")] [TestCase("https://example.com", WebhookEvent.MediaSave, "00000000-0000-0000-0000-000000500000")] - public async Task Enum_Stored_as_string(string url, WebhookEvent webhookEvent, Guid key) + public async Task Can_Create_And_Get(string url, WebhookEvent webhookEvent, Guid key) { var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, webhookEvent, key)); var webhook = await WebhookService.GetAsync(createdWebhook.Key); @@ -31,4 +31,38 @@ public async Task Enum_Stored_as_string(string url, WebhookEvent webhookEvent, G Assert.AreEqual(key, webhook.EntityKey); }); } + + [Test] + public async Task Can_Get_Multiple() + { + var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentPublish, Guid.NewGuid())); + var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentDelete, Guid.NewGuid())); + var keys = new List { createdWebhookOne.Key, createdWebhookTwo.Key }; + var webhooks = await WebhookService.GetMultipleAsync(keys); + + Assert.Multiple(() => + { + Assert.IsNotEmpty(webhooks); + Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookOne.Key)); + Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookTwo.Key)); + }); + } + + [Test] + [TestCase("https://example.com", WebhookEvent.ContentPublish, "00000000-0000-0000-0000-010000000000")] + [TestCase("https://example.com", WebhookEvent.ContentDelete, "00000000-0000-0000-0000-000200000000")] + [TestCase("https://example.com", WebhookEvent.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] + [TestCase("https://example.com", WebhookEvent.MediaDelete, "00000000-0000-0000-0000-000004000000")] + [TestCase("https://example.com", WebhookEvent.MediaSave, "00000000-0000-0000-0000-000000500000")] + public async Task Can_Create_And_Delete(string url, WebhookEvent webhookEvent, Guid key) + { + var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, webhookEvent, key)); + var webhook = await WebhookService.GetAsync(createdWebhook.Key); + + Assert.IsNotNull(webhook); + await WebhookService.DeleteAsync(webhook.Key); + var deletedWebhook = await WebhookService.GetAsync(createdWebhook.Key); + Assert.IsNull(deletedWebhook); + + } } From 6aa3f2bab15fb368e39590f047978187ca8f60a3 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 11 Sep 2023 09:30:30 +0200 Subject: [PATCH 014/102] Rework Webhooks to be able to have multiple entity keys --- src/Umbraco.Core/Models/Webhook.cs | 12 +++++------ .../Persistence/Constants-DatabaseSchema.cs | 1 + .../Install/DatabaseSchemaCreator.cs | 1 + .../Persistence/Dtos/WebhookEntityKeyDto.cs | 20 ++++++++++++++++++ .../Persistence/Factories/WebhookFactory.cs | 13 ++++++++---- .../Implement/WebhookRepository.cs | 21 ++++++++++++------- .../Services/WebhookServiceTests.cs | 21 +++++++++++++------ 7 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/WebhookEntityKeyDto.cs diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index b6a6fa005963..39c2ae48a8c5 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -9,13 +9,13 @@ public class Webhook : EntityBase { private string _url; private WebhookEvent _webHookEvent; - private Guid _entityKey; + private Guid[] _entityKeys; - public Webhook(string url, WebhookEvent webHookEvent, Guid entityKey) + public Webhook(string url, WebhookEvent webHookEvent, Guid[]? entityKeys = null) { _url = url; _webHookEvent = webHookEvent; - _entityKey = entityKey; + _entityKeys = entityKeys ?? Array.Empty(); } [DataMember] @@ -33,9 +33,9 @@ public WebhookEvent Event } [DataMember] - public Guid EntityKey + public Guid[] EntityKeys { - get => _entityKey; - set => SetPropertyValueAndDetectChanges(value, ref _entityKey, nameof(EntityKey)); + get => _entityKeys; + set => SetPropertyValueAndDetectChanges(value, ref _entityKeys!, nameof(EntityKeys)); } } diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 4999142a8c8b..bd7762267d08 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -87,6 +87,7 @@ public static class Tables public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; public const string Webhook = TableNamePrefix + "Webhook"; + public const string WebhookEntityKey = TableNamePrefix + "WebhookEntityKey"; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 6f3bc845d50e..20dd2de5c9bf 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -84,6 +84,7 @@ public class DatabaseSchemaCreator typeof(CreatedPackageSchemaDto), typeof(UserGroup2LanguageDto), typeof(WebhookDto), + typeof(WebhookEntityKeyDto) }; private readonly IUmbracoDatabase _database; diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookEntityKeyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookEntityKeyDto.cs new file mode 100644 index 000000000000..df993079df3d --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookEntityKeyDto.cs @@ -0,0 +1,20 @@ +using System.Data; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + + +[TableName(Constants.DatabaseSchema.Tables.WebhookEntityKey)] +[ExplicitColumns] +public class WebhookEntityKeyDto +{ + [Column("webhookId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_webhookEntityKey2webhook", OnColumns = "webhookId, entityKey")] + [ForeignKey(typeof(WebhookDto), OnDelete = Rule.Cascade)] + public int WebhookId { get; set; } + + [Column("entityKey")] + public Guid EntityKey { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index c6a918e42ead..1d75c13bc0c9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -5,10 +5,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class WebhookFactory { - public static Webhook BuildEntity(WebhookDto dto) + public static Webhook BuildEntity(WebhookDto dto, IEnumerable? entityKey2WebhookDtos = null) { - var entity = new Webhook(dto.Url, Enum.Parse(dto.Event), dto.EntityKey); - + var entity = new Webhook(dto.Url, Enum.Parse(dto.Event), entityKey2WebhookDtos?.Select(x => x.EntityKey).ToArray()); try { entity.DisableChangeTracking(); @@ -32,7 +31,6 @@ public static WebhookDto BuildDto(Webhook webhook) { Url = webhook.Url, Event = webhook.Event.ToString(), - EntityKey = webhook.EntityKey, Key = webhook.Key, }; if (webhook.HasIdentity) @@ -42,4 +40,11 @@ public static WebhookDto BuildDto(Webhook webhook) return dto; } + + public static IEnumerable BuildEntityKey2WebhookDtos(Webhook webhook, int webhookId) => + webhook.EntityKeys.Select(x => new WebhookEntityKeyDto + { + EntityKey = x, + WebhookId = webhookId + }); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index 9e15b1cd33c3..c676ccb60970 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -25,9 +25,13 @@ public WebhookRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILog sql.Where(GetBaseWhereClause(), new { key }); WebhookDto? dto = Database.FirstOrDefault(sql); - return dto == null - ? null - : DtoToEntity(dto); + + if (dto is not null) + { + return DtoToEntity(dto); + } + + return null; } protected override IEnumerable PerformGetAll(params Guid[]? ids) @@ -54,10 +58,12 @@ protected override void PersistNewItem(Webhook entity) { entity.AddingEntity(); - WebhookDto dto = WebhookFactory.BuildDto(entity); + WebhookDto webhookDto = WebhookFactory.BuildDto(entity); - var id = Convert.ToInt32(Database.Insert(dto)); + var id = Convert.ToInt32(Database.Insert(webhookDto)); entity.Id = id; + IEnumerable buildEntityKey2WebhookDtos = WebhookFactory.BuildEntityKey2WebhookDtos(entity, id); + Database.InsertBulk(buildEntityKey2WebhookDtos); entity.ResetDirtyProperties(); } @@ -111,9 +117,10 @@ protected override void PersistDeletedItem(Webhook entity) entity.DeleteDate = DateTime.Now; } - private static Webhook DtoToEntity(WebhookDto dto) + private Webhook DtoToEntity(WebhookDto dto) { - Webhook entity = WebhookFactory.BuildEntity(dto); + List webhookEntityKeyDtos = Database.Fetch("WHERE webhookId = @webhookId", new { webhookId = dto.Id }); + Webhook entity = WebhookFactory.BuildEntity(dto, webhookEntityKeyDtos); // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index 20ec1f64dacf..dfd02a558e65 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -20,7 +20,7 @@ public class WebhookServiceTests : UmbracoIntegrationTest [TestCase("https://example.com", WebhookEvent.MediaSave, "00000000-0000-0000-0000-000000500000")] public async Task Can_Create_And_Get(string url, WebhookEvent webhookEvent, Guid key) { - var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, webhookEvent, key)); + var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, webhookEvent, new[] { key })); var webhook = await WebhookService.GetAsync(createdWebhook.Key); Assert.Multiple(() => @@ -28,15 +28,15 @@ public async Task Can_Create_And_Get(string url, WebhookEvent webhookEvent, Guid Assert.IsNotNull(webhook); Assert.AreEqual(webhookEvent, webhook.Event); Assert.AreEqual(url, webhook.Url); - Assert.AreEqual(key, webhook.EntityKey); + Assert.IsTrue(webhook.EntityKeys.Contains(key)); }); } [Test] public async Task Can_Get_Multiple() { - var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentPublish, Guid.NewGuid())); - var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentDelete, Guid.NewGuid())); + var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentPublish, new[] { Guid.NewGuid() })); + var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentDelete, new[] { Guid.NewGuid() })); var keys = new List { createdWebhookOne.Key, createdWebhookTwo.Key }; var webhooks = await WebhookService.GetMultipleAsync(keys); @@ -54,15 +54,24 @@ public async Task Can_Get_Multiple() [TestCase("https://example.com", WebhookEvent.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] [TestCase("https://example.com", WebhookEvent.MediaDelete, "00000000-0000-0000-0000-000004000000")] [TestCase("https://example.com", WebhookEvent.MediaSave, "00000000-0000-0000-0000-000000500000")] - public async Task Can_Create_And_Delete(string url, WebhookEvent webhookEvent, Guid key) + public async Task Can_Delete(string url, WebhookEvent webhookEvent, Guid key) { - var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, webhookEvent, key)); + var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, webhookEvent, new[] { key })); var webhook = await WebhookService.GetAsync(createdWebhook.Key); Assert.IsNotNull(webhook); await WebhookService.DeleteAsync(webhook.Key); var deletedWebhook = await WebhookService.GetAsync(createdWebhook.Key); Assert.IsNull(deletedWebhook); + } + + [Test] + public async Task Can_Create_With_No_EntityKeys() + { + var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentPublish)); + var webhook = await WebhookService.GetAsync(createdWebhook.Key); + Assert.IsNotNull(webhook); + Assert.IsEmpty(webhook.EntityKeys); } } From 4d904832dbf4b453ec5e119841efd0c6de2febf1 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 11 Sep 2023 10:03:24 +0200 Subject: [PATCH 015/102] Implement GetAll functionality --- src/Umbraco.Core/Services/IWebHookService.cs | 2 ++ src/Umbraco.Core/Services/WebhookService.cs | 8 ++++++++ .../Repositories/Implement/WebhookRepository.cs | 2 +- .../Services/WebhookServiceTests.cs | 17 +++++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs index f8f92c3dd651..8f5b0daf44ff 100644 --- a/src/Umbraco.Core/Services/IWebHookService.cs +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -11,4 +11,6 @@ public interface IWebHookService Task GetAsync(Guid key); Task> GetMultipleAsync(IEnumerable keys); + + Task> GetAllAsync(); } diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 9b31c9d04756..f4294e0a1708 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -52,4 +52,12 @@ public Task> GetMultipleAsync(IEnumerable keys) scope.Complete(); return Task.FromResult(webhooks); } + + public Task> GetAllAsync() + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + IEnumerable webhooks = _webhookRepository.GetMany(); + scope.Complete(); + return Task.FromResult(webhooks); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index c676ccb60970..42c2edeb6be6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -38,7 +38,7 @@ protected override IEnumerable PerformGetAll(params Guid[]? ids) { Sql sql = GetBaseQuery(false); - List? dtos = Database.Fetch(sql); + List dtos = Database.Fetch(sql); return dtos.Select(DtoToEntity); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index dfd02a558e65..bb3b1602d807 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -48,6 +48,23 @@ public async Task Can_Get_Multiple() }); } + [Test] + public async Task Can_Get_All() + { + var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentPublish, new[] { Guid.NewGuid() })); + var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentDelete, new[] { Guid.NewGuid() })); + var createdWebhookThree = await WebhookService.CreateAsync(new Webhook("https://example.com", WebhookEvent.ContentUnpublish, new[] { Guid.NewGuid() })); + var webhooks = await WebhookService.GetAllAsync(); + + Assert.Multiple(() => + { + Assert.IsNotEmpty(webhooks); + Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookOne.Key)); + Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookTwo.Key)); + Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookThree.Key)); + }); + } + [Test] [TestCase("https://example.com", WebhookEvent.ContentPublish, "00000000-0000-0000-0000-010000000000")] [TestCase("https://example.com", WebhookEvent.ContentDelete, "00000000-0000-0000-0000-000200000000")] From e457600d316c42e0f4308a3fe6c07be157a7d67f Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 11 Sep 2023 10:45:09 +0200 Subject: [PATCH 016/102] Implement webhook controller --- .../Controllers/WebHookController.cs | 54 +++++++++++++++++++ .../Mapping/WebhookMapDefinition.cs | 30 +++++++++++ .../Models/WebhookViewModel.cs | 12 +++++ 3 files changed, 96 insertions(+) create mode 100644 src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs create mode 100644 src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs create mode 100644 src/Umbraco.Web.Common/Models/WebhookViewModel.cs diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs new file mode 100644 index 000000000000..481311bbd0bf --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.Models; + +namespace Umbraco.Cms.Web.BackOffice.Controllers; + + +public class WebHookController : UmbracoApiController +{ + private readonly IWebHookService _webHookService; + private readonly IUmbracoMapper _umbracoMapper; + + public WebHookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper) + { + _webHookService = webHookService; + _umbracoMapper = umbracoMapper; + } + + [HttpPost] + public async Task Create(WebhookViewModel webhookViewModel) + { + Webhook webhook = _umbracoMapper.Map(webhookViewModel)!; + await _webHookService.CreateAsync(webhook); + + return Ok(); + } + + [HttpGet] + public async Task GetByKey(Guid key) + { + Webhook? webhook = await _webHookService.GetAsync(key); + + return webhook is null ? NotFound() : Ok(webhook); + } + + [HttpGet] + public async Task Get() + { + IEnumerable webhooks = await _webHookService.GetAllAsync(); + + return Ok(webhooks); + } + + [HttpDelete] + public async Task Delete(Guid key) + { + await _webHookService.DeleteAsync(key); + + return Ok(); + } +} diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs new file mode 100644 index 000000000000..f34cc1336760 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -0,0 +1,30 @@ +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Web.Common.Models; + +namespace Umbraco.Cms.Web.BackOffice.Mapping; + +public class WebhookMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new Webhook(string.Empty, WebhookEvent.ContentDelete), Map); + mapper.Define((_, _) => new WebhookViewModel(), Map); + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -Key -UpdateDate + private void Map(WebhookViewModel source, Webhook target, MapperContext context) + { + target.EntityKeys = source.EntityKeys; + target.Event = source.Event; + target.Url = source.Url; + } + + // Umbraco.Code.MapAll + private void Map(Webhook source, WebhookViewModel target, MapperContext context) + { + target.EntityKeys = source.EntityKeys; + target.Event = source.Event; + target.Url = source.Url; + } +} diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs new file mode 100644 index 000000000000..62ca689b874a --- /dev/null +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Web.Common.Models; + +public class WebhookViewModel +{ + public string Url { get; set; } = string.Empty; + + public WebhookEvent Event { get; set; } + + public Guid[] EntityKeys { get; set; } = Array.Empty(); +} From e24cf0ea277e6e7cbdb6b39f4b31ae2f525746d4 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 11 Sep 2023 11:08:05 +0200 Subject: [PATCH 017/102] Imeplement get all events action --- .../Controllers/BackOfficeServerVariables.cs | 4 ++++ .../Controllers/WebHookController.cs | 21 +++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 6dbd5f1e7946..13d41177baef 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -585,6 +585,10 @@ internal async Task> GetServerVariablesAsync() "mediaPickerThreeBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.UploadMedia(null!)) }, + { + "webhooksApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAll()) + }, } }, { diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs index 481311bbd0bf..f5c1a2ce87e6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -7,7 +7,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers; - public class WebHookController : UmbracoApiController { private readonly IWebHookService _webHookService; @@ -19,6 +18,14 @@ public WebHookController(IWebHookService webHookService, IUmbracoMapper umbracoM _umbracoMapper = umbracoMapper; } + [HttpGet] + public async Task GetAll() + { + IEnumerable webhooks = await _webHookService.GetAllAsync(); + + return Ok(webhooks); + } + [HttpPost] public async Task Create(WebhookViewModel webhookViewModel) { @@ -36,14 +43,6 @@ public async Task GetByKey(Guid key) return webhook is null ? NotFound() : Ok(webhook); } - [HttpGet] - public async Task Get() - { - IEnumerable webhooks = await _webHookService.GetAllAsync(); - - return Ok(webhooks); - } - [HttpDelete] public async Task Delete(Guid key) { @@ -51,4 +50,8 @@ public async Task Delete(Guid key) return Ok(); } + + // TODO: This should probably be handled by the NewtonsoftJsonOutputFormatter instead + [HttpGet] + public async Task GetEvents() => Ok(Enum.GetValues(typeof(WebhookEvent)).Cast().Select(x => x.ToString())); } From 8f3fc3b45d819735a9d8b0a44fccaf613a58bf97 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 11 Sep 2023 13:18:44 +0200 Subject: [PATCH 018/102] Add equalityComparer deletegate to Webhook --- src/Umbraco.Core/Models/Webhook.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index 39c2ae48a8c5..0c3707883a53 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -1,5 +1,6 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models; @@ -11,6 +12,12 @@ public class Webhook : EntityBase private WebhookEvent _webHookEvent; private Guid[] _entityKeys; + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> _guidEnumerableComparer = + new( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); + public Webhook(string url, WebhookEvent webHookEvent, Guid[]? entityKeys = null) { _url = url; @@ -36,6 +43,6 @@ public WebhookEvent Event public Guid[] EntityKeys { get => _entityKeys; - set => SetPropertyValueAndDetectChanges(value, ref _entityKeys!, nameof(EntityKeys)); + set => SetPropertyValueAndDetectChanges(value, ref _entityKeys!, nameof(EntityKeys), _guidEnumerableComparer); } } From e04ec6b65ac6989a241845a7b44ab93a53890f53 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 11 Sep 2023 13:19:06 +0200 Subject: [PATCH 019/102] Add datacontract attirbutes to properties --- src/Umbraco.Web.Common/Models/WebhookViewModel.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs index 62ca689b874a..333db45548cd 100644 --- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -1,12 +1,20 @@ -using Umbraco.Cms.Core.Models; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Web.Common.Models; +[DataContract] public class WebhookViewModel { + [DataMember(Name = "url")] public string Url { get; set; } = string.Empty; + [DataMember(Name = "event")] + [JsonConverter(typeof(StringEnumConverter))] public WebhookEvent Event { get; set; } + [DataMember(Name = "entityKeys")] public Guid[] EntityKeys { get; set; } = Array.Empty(); } From b0e7ddb855c61924565e4ac5169739c7680086b8 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 11 Sep 2023 14:09:08 +0200 Subject: [PATCH 020/102] Implement backoffice webhooks tree --- src/Umbraco.Core/Constants-Applications.cs | 2 + src/Umbraco.Core/Models/Webhook.cs | 23 ++++--- .../Persistence/Dtos/WebhookDto.cs | 3 + .../Persistence/Factories/WebhookFactory.cs | 3 +- .../Controllers/WebHookController.cs | 7 ++- .../UmbracoBuilderExtensions.cs | 4 +- .../Mapping/WebhookMapDefinition.cs | 2 + .../Trees/WebhooksTreeController.cs | 62 +++++++++++++++++++ .../Models/WebhookViewModel.cs | 3 + 9 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index dc36715585e6..aa1f19c7912a 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -147,6 +147,8 @@ public static class Trees public const string LogViewer = "logViewer"; + public const string Webhooks = "webhooks"; + public static class Groups { public const string Settings = "settingsGroup"; diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index 0c3707883a53..690c4c842355 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -4,13 +4,12 @@ namespace Umbraco.Cms.Core.Models; -[Serializable] -[DataContract(IsReference = true)] public class Webhook : EntityBase { private string _url; - private WebhookEvent _webHookEvent; + private WebhookEvent _event; private Guid[] _entityKeys; + private bool _enabled; // Custom comparer for enumerable private static readonly DelegateEqualityComparer> _guidEnumerableComparer = @@ -18,31 +17,35 @@ public class Webhook : EntityBase (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), enum1 => enum1.GetHashCode()); - public Webhook(string url, WebhookEvent webHookEvent, Guid[]? entityKeys = null) + public Webhook(string url, WebhookEvent webhookEvent, bool? enabled = null, Guid[]? entityKeys = null) { _url = url; - _webHookEvent = webHookEvent; + _event = webhookEvent; _entityKeys = entityKeys ?? Array.Empty(); + _enabled = enabled ?? false; } - [DataMember] public string Url { get => _url; set => SetPropertyValueAndDetectChanges(value, ref _url!, nameof(Url)); } - [DataMember] public WebhookEvent Event { - get => _webHookEvent; - set => SetPropertyValueAndDetectChanges(value, ref _webHookEvent!, nameof(Event)); + get => _event; + set => SetPropertyValueAndDetectChanges(value, ref _event!, nameof(Event)); } - [DataMember] public Guid[] EntityKeys { get => _entityKeys; set => SetPropertyValueAndDetectChanges(value, ref _entityKeys!, nameof(EntityKeys), _guidEnumerableComparer); } + + public bool Enabled + { + get => _enabled; + set => SetPropertyValueAndDetectChanges(value, ref _enabled, nameof(Event)); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs index b4cb692b5a9e..b687cedd089d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -28,5 +28,8 @@ internal class WebhookDto [Column(Name = "entityKey")] public Guid EntityKey { get; set; } + + [Column(Name = "enabled")] + public bool Enabled { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index 1d75c13bc0c9..ffd81d6affef 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -7,7 +7,7 @@ internal static class WebhookFactory { public static Webhook BuildEntity(WebhookDto dto, IEnumerable? entityKey2WebhookDtos = null) { - var entity = new Webhook(dto.Url, Enum.Parse(dto.Event), entityKey2WebhookDtos?.Select(x => x.EntityKey).ToArray()); + var entity = new Webhook(dto.Url, Enum.Parse(dto.Event), dto.Enabled, entityKey2WebhookDtos?.Select(x => x.EntityKey).ToArray()); try { entity.DisableChangeTracking(); @@ -32,6 +32,7 @@ public static WebhookDto BuildDto(Webhook webhook) Url = webhook.Url, Event = webhook.Event.ToString(), Key = webhook.Key, + Enabled = webhook.Enabled }; if (webhook.HasIdentity) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs index f5c1a2ce87e6..1646c32f0ad9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -23,7 +23,9 @@ public async Task GetAll() { IEnumerable webhooks = await _webHookService.GetAllAsync(); - return Ok(webhooks); + List webhookViewModels = _umbracoMapper.MapEnumerable(webhooks); + + return Ok(webhookViewModels); } [HttpPost] @@ -53,5 +55,6 @@ public async Task Delete(Guid key) // TODO: This should probably be handled by the NewtonsoftJsonOutputFormatter instead [HttpGet] - public async Task GetEvents() => Ok(Enum.GetValues(typeof(WebhookEvent)).Cast().Select(x => x.ToString())); + public async Task GetEvents() => + Ok(Enum.GetValues(typeof(WebhookEvent)).Cast().Select(x => x.ToString())); } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 994493e761cc..c97396349526 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -4,15 +4,16 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Infrastructure.Examine.DependencyInjection; -using Umbraco.Cms.Infrastructure.Templates.PartialViews; using Umbraco.Cms.Infrastructure.WebAssets; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.Install; +using Umbraco.Cms.Web.BackOffice.Mapping; using Umbraco.Cms.Web.BackOffice.Middleware; using Umbraco.Cms.Web.BackOffice.ModelsBuilder; using Umbraco.Cms.Web.BackOffice.Routing; @@ -91,6 +92,7 @@ public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder) builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.WithCollectionBuilder().Add(); // register back office trees // the collection builder only accepts types inheriting from TreeControllerBase diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index f34cc1336760..dbefe8156ed5 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -18,6 +18,7 @@ private void Map(WebhookViewModel source, Webhook target, MapperContext context) target.EntityKeys = source.EntityKeys; target.Event = source.Event; target.Url = source.Url; + target.Enabled = source.Enabled; } // Umbraco.Code.MapAll @@ -26,5 +27,6 @@ private void Map(Webhook source, WebhookViewModel target, MapperContext context) target.EntityKeys = source.EntityKeys; target.Event = source.Event; target.Url = source.Url; + target.Enabled = source.Enabled; } } diff --git a/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs new file mode 100644 index 000000000000..82cad01c7901 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Trees; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Web.BackOffice.Trees; + +[Authorize(Policy = AuthorizationPolicies.TreeAccessLogs)] +[Tree(Constants.Applications.Settings, Constants.Trees.Webhooks, SortOrder = 9, TreeGroup = Constants.Trees.Groups.Settings)] +[PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] +[CoreTree] +public class WebhooksTreeController : TreeController +{ + private readonly IMenuItemCollectionFactory _menuItemCollectionFactory; + + public WebhooksTreeController( + ILocalizedTextService localizedTextService, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + IEventAggregator eventAggregator, + IMenuItemCollectionFactory menuItemCollectionFactory) + : base(localizedTextService, umbracoApiControllerTypeCollection, eventAggregator) => + _menuItemCollectionFactory = menuItemCollectionFactory; + + protected override ActionResult GetTreeNodes(string id, FormCollection queryStrings) => + //We don't have any child nodes & only use the root node to load a custom UI + new TreeNodeCollection(); + + protected override ActionResult GetMenuForNode(string id, FormCollection queryStrings) => + //We don't have any menu item options (such as create/delete/reload) & only use the root node to load a custom UI + _menuItemCollectionFactory.Create(); + + /// + /// Helper method to create a root model for a tree + /// + /// + protected override ActionResult CreateRootNode(FormCollection queryStrings) + { + ActionResult rootResult = base.CreateRootNode(queryStrings); + if (!(rootResult.Result is null)) + { + return rootResult; + } + + TreeNode? root = rootResult.Value; + + if (root is not null) + { + // This will load in a custom UI instead of the dashboard for the root node + root.RoutePath = $"{Constants.Applications.Settings}/{Constants.Trees.Webhooks}/overview"; + root.Icon = Constants.Icons.ListView; + root.HasChildren = false; + root.MenuUrl = null; + } + + return root; + } +} diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs index 333db45548cd..e0a6f9111442 100644 --- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -17,4 +17,7 @@ public class WebhookViewModel [DataMember(Name = "entityKeys")] public Guid[] EntityKeys { get; set; } = Array.Empty(); + + [DataMember(Name = "enabled")] + public bool Enabled { get; set; } } From bef79a1c267261e926b8eff8fe6b2f71c9852f73 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 11 Sep 2023 15:06:58 +0200 Subject: [PATCH 021/102] Implement first webhooks menu --- .../EmbeddedResources/Lang/en_us.xml | 5 + .../Mapping/WebhookMapDefinition.cs | 2 + .../Models/WebhookViewModel.cs | 3 + .../src/common/resources/webhooks.resource.js | 35 +++++ .../src/views/webhooks/overview.controller.js | 146 ++++++++++++++++++ .../src/views/webhooks/overview.html | 72 +++++++++ 6 files changed, 263 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index de472144a1fb..211c5570452c 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1965,6 +1965,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]> Changing a data type with stored values is disabled. To allow this you can change the Umbraco:CMS:DataTypes:CanBeChanged setting in appsettings.json. + + Create webhook + Add language ISO code @@ -2105,10 +2108,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Stylesheets Templates Log Viewer + Webhooks Users Settings Templating Third Party + Webhooks New update ready diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index dbefe8156ed5..6e007f009e59 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -19,6 +19,7 @@ private void Map(WebhookViewModel source, Webhook target, MapperContext context) target.Event = source.Event; target.Url = source.Url; target.Enabled = source.Enabled; + target.Key = source.Key ?? Guid.NewGuid(); } // Umbraco.Code.MapAll @@ -28,5 +29,6 @@ private void Map(Webhook source, WebhookViewModel target, MapperContext context) target.Event = source.Event; target.Url = source.Url; target.Enabled = source.Enabled; + target.Key = source.Key; } } diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs index e0a6f9111442..ce1d8dfe68c6 100644 --- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -8,6 +8,9 @@ namespace Umbraco.Cms.Web.Common.Models; [DataContract] public class WebhookViewModel { + [DataMember(Name = "url")] + public Guid? Key { get; set; } + [DataMember(Name = "url")] public string Url { get; set; } = string.Empty; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js new file mode 100644 index 000000000000..8cb38bbabfd1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js @@ -0,0 +1,35 @@ +function webhooksResource($q, $http, umbRequestHelper) { + return { + getByKey(key) { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetByKey', {key})), + 'Failed to get webhooks', + ); + }, + getAll(pageNumber, pageSize) { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetAll', {pageNumber, pageSize})), + 'Failed to get webhooks', + ); + }, + create(webhook) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'Create'), webhook), + `Failed to save webhook id ${webhook.id}`, + ); + }, + delete(key) { + return umbRequestHelper.resourcePromise( + $http.delete(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'Delete', {key})), + `Failed to delete webhook id ${id}`, + ); + }, + getAllEvents() { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetEvents')), + 'Failed to get events', + ); + }, + }; +} +angular.module('umbraco.resources').factory('webhooksResource', webhooksResource); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js new file mode 100644 index 000000000000..4d48aeff6ef3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js @@ -0,0 +1,146 @@ +(function () { + "use strict"; + + function OverviewController($q, webhooksResource, notificationsService, editorService, overlayService, localizationService, $scope) { + var vm = this; + vm.loading = true; + + vm.openWebhookOverlay = openWebhookOverlay; + vm.deleteWebhook = deleteWebhook; + + vm.pagination = { + pageNumber: 1, + pageSize: 25, + }; + + vm.page = {}; + vm.webhooks = []; + vm.events = []; + var promises = []; + + // Localize labels + promises.push(localizationService.localize("treeHeaders_webhooks").then(function (value) { + vm.page.name = value; + $scope.$emit("$changeTitle", value); + })); + + console.log(this.webhooks); + const loadEvents = () => webhooksResource.getAllEvents() + .then((data) => { + this.events = data; + }); + + // const handleSubmissionError = (model, errorMessage) => { + // notificationsService.error(errorMessage); + // model.disableSubmitButton = false; + // model.submitButtonState = 'error'; + // } + + function openWebhookOverlay (webhook) { + editorService.open({ + title: webhook ? 'Edit webhook' : 'Add webhook', + position: 'right', + size: 'small', + submitButtonLabel: webhook ? 'Save' : 'Create', + view: EditOverlay, + events: this.events, + contentType: webhook ? webhook.contentType : null, + webhook: webhook ? { + contentType: webhook.contentType ? webhook.contentType.id : null, + enabled: webhook.enabled, + event: webhook.event.id, + id: webhook.id, + url: webhook.url, + } : {enabled: true}, + submit: (model) => { + model.disableSubmitButton = true; + model.submitButtonState = 'busy'; + if (!model.webhook.url) { + //Due to validation url will only be populated if it's valid, hence we can make do with checking url is there + handleSubmissionError(model, 'Please provide a valid URL. Did you include https:// ?'); + return; + } + if (!model.webhook.event) { + handleSubmissionError(model, 'Please provide the event for which the webhook should trigger'); + return; + } + headlessWebhooksResource.save(model.webhook) + .then(() => { + this.goToPage(1); + + notificationsService.success('Webhook saved.'); + editorService.close(); + }, x => { + let errorMessage = undefined; + if (x.data.ModelState) { + errorMessage = `Message: ${Object.values(x.data.ModelState).flat().join(' ')}`; + } + handleSubmissionError(model, `Error saving webhook. ${errorMessage ?? ''}`); + }); + }, + close: () => { + editorService.close(); + }, + }); + } + + function loadWebhooks(){ + return webhooksResource + .getAll(vm.pagination.pageNumber, vm.pagination.pageSize) + .then((result) => { + vm.webhooks = result; + + vm.pagination.pageNumber = result.pageNumber; + vm.pagination.totalItems = result.totalItems; + vm.pagination.totalPages = result.totalPages; + }); + } + + function deleteWebhook (webhook) { + overlayService.open({ + title: 'Confirm delete webhook', + content: 'Are you sure you want to delete the webhook?', + submitButtonLabel: 'Yes, delete', + submitButtonStyle: 'danger', + closeButtonLabel: 'Cancel', + submit: () => { + webhooksResource.deleteById(webhook.id) + .then(() => { + const index = this.webhooks.indexOf(webhook); + this.webhooks.splice(index, 1); + + notificationsService.success('Webhook deleted.'); + overlayService.close(); + }, () => { + notificationsService.error('Error deleting webhook.'); + }); + }, + close: () => { + overlayService.close(); + }, + }); + }; + + this.previousPage = () => this.goToPage(this.pagination.pageNumber - 1); + this.nextPage = () => this.goToPage(this.pagination.pageNumber + 1); + + this.goToPage = (pageNumber) => { + this.pagination.pageNumber = pageNumber; + this.loading = true; + loadWebhooks().then(() => { + this.loading = false; + }); + }; + + $q.all([ + promises, + loadWebhooks(), + loadEvents(), + ]).then(() => { + vm.loading = false; + }); + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.OverviewController", OverviewController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html new file mode 100644 index 000000000000..2c13ec2940db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html @@ -0,0 +1,72 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EnabledEventUrlTypes
+ + + + + {{ webhook.event }} + + {{ webhook.url }} + + Dummy content type name + + + +
+ +
+ +
+ +
From 5097145c7336438cbcbdde13897e3e5a8631e40b Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:31:57 +0200 Subject: [PATCH 022/102] Make WebHookController authorized --- src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs | 3 +-- src/Umbraco.Web.Common/Models/WebhookViewModel.cs | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs index 1646c32f0ad9..38c966cb8f3b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -2,12 +2,11 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Models; namespace Umbraco.Cms.Web.BackOffice.Controllers; -public class WebHookController : UmbracoApiController +public class WebHookController : UmbracoAuthorizedJsonController { private readonly IWebHookService _webHookService; private readonly IUmbracoMapper _umbracoMapper; diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs index ce1d8dfe68c6..2a1731d2e1da 100644 --- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -1,6 +1,4 @@ using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Web.Common.Models; @@ -8,14 +6,13 @@ namespace Umbraco.Cms.Web.Common.Models; [DataContract] public class WebhookViewModel { - [DataMember(Name = "url")] + [DataMember(Name = "key")] public Guid? Key { get; set; } [DataMember(Name = "url")] public string Url { get; set; } = string.Empty; [DataMember(Name = "event")] - [JsonConverter(typeof(StringEnumConverter))] public WebhookEvent Event { get; set; } [DataMember(Name = "entityKeys")] From adbf50656a6e8170b342488b93952c4b4f739b43 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:45:48 +0200 Subject: [PATCH 023/102] Update to have tabs with webhooks and logs --- .../EmbeddedResources/Lang/en_us.xml | 1 + .../src/common/resources/webhooks.resource.js | 18 +- .../webhooks/overlays/edit.controller.js | 53 ++++++ .../src/views/webhooks/overlays/edit.html | 96 ++++++++++ .../src/views/webhooks/overview.controller.js | 170 +++++------------- .../src/views/webhooks/overview.html | 59 +----- .../src/views/webhooks/webhooks.controller.js | 156 ++++++++++++++++ .../src/views/webhooks/webhooks.html | 57 ++++++ 8 files changed, 424 insertions(+), 186 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 211c5570452c..c6a6c804dfef 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1967,6 +1967,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Create webhook + Logs Add language diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js index 8cb38bbabfd1..dd289fc2797c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js @@ -3,33 +3,39 @@ getByKey(key) { return umbRequestHelper.resourcePromise( $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetByKey', {key})), - 'Failed to get webhooks', + 'Failed to get webhooks' ); }, getAll(pageNumber, pageSize) { return umbRequestHelper.resourcePromise( $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetAll', {pageNumber, pageSize})), - 'Failed to get webhooks', + 'Failed to get webhooks' ); }, create(webhook) { return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'Create'), webhook), - `Failed to save webhook id ${webhook.id}`, + `Failed to save webhook id ${webhook.id}` + ); + }, + update(webhook) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'Create'), webhook), + `Failed to save webhook id ${webhook.id}` ); }, delete(key) { return umbRequestHelper.resourcePromise( $http.delete(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'Delete', {key})), - `Failed to delete webhook id ${id}`, + `Failed to delete webhook id ${key}` ); }, getAllEvents() { return umbRequestHelper.resourcePromise( $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetEvents')), - 'Failed to get events', + 'Failed to get events' ); - }, + } }; } angular.module('umbraco.resources').factory('webhooksResource', webhooksResource); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js new file mode 100644 index 000000000000..fab54bc2cb22 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js @@ -0,0 +1,53 @@ +(function () { + "use strict"; + function EditController($scope, editorService) { + this.openContentTypePicker = () => { + const isContent = $scope.model.webhook ? $scope.model.webhook.event.split('.')[0] === 'content' : null; + editorService.treePicker({ + section: 'settings', + treeAlias: isContent ? 'documentTypes' : 'mediaTypes', + entityType: isContent ? 'DocumentType' : 'MediaType', + multiPicker: false, + submit(model) { + // sets $scope.model.contentType to model.selection[0] + [$scope.model.contentType] = model.selection; + $scope.model.webhook.contentType = $scope.model.contentType.id; + editorService.close(); + }, + close() { + editorService.close(); + } + }); + }; + + this.clearContentType = () => { + delete $scope.model.webhook.contentType; + delete $scope.model.contentType; + }; + + this.eventChanged = (newValue, oldValue) => { + if (oldValue && newValue) { + if (oldValue.split('.')[0] !== newValue.split('.')[0]) { + this.clearContentType(); + } + } + if (!newValue) { + this.clearContentType(); + } + }; + + this.close = () => { + if ($scope.model.close) { + $scope.model.close(); + } + }; + + this.submit = () => { + if ($scope.model.submit) { + $scope.model.submit($scope.model); + } + }; + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.EditController", EditController); +}); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html new file mode 100644 index 000000000000..d8574a5fb096 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html @@ -0,0 +1,96 @@ +
+ + + + + + + + +
+ + + + + + + + + + + + + Add + + Please select an event first. + + + + + + +
+
+
+
+ + + + + + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js index 4d48aeff6ef3..103dfd71aa49 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js @@ -1,144 +1,62 @@ (function () { "use strict"; - function OverviewController($q, webhooksResource, notificationsService, editorService, overlayService, localizationService, $scope) { + function OverviewController($q, $location, $routeParams, webhooksResource, notificationsService, editorService, overlayService, localizationService) { var vm = this; - vm.loading = true; - - vm.openWebhookOverlay = openWebhookOverlay; - vm.deleteWebhook = deleteWebhook; + vm.page = {}; + vm.page.labels = {}; + vm.page.name = ""; + vm.page.navigation = []; + let webhookUri = $routeParams.method; - vm.pagination = { - pageNumber: 1, - pageSize: 25, - }; - vm.page = {}; - vm.webhooks = []; - vm.events = []; - var promises = []; + onInit(); - // Localize labels - promises.push(localizationService.localize("treeHeaders_webhooks").then(function (value) { - vm.page.name = value; - $scope.$emit("$changeTitle", value); - })); + function onInit() { - console.log(this.webhooks); - const loadEvents = () => webhooksResource.getAllEvents() - .then((data) => { - this.events = data; - }); + loadNavigation(); - // const handleSubmissionError = (model, errorMessage) => { - // notificationsService.error(errorMessage); - // model.disableSubmitButton = false; - // model.submitButtonState = 'error'; - // } + setPageName(); + } - function openWebhookOverlay (webhook) { - editorService.open({ - title: webhook ? 'Edit webhook' : 'Add webhook', - position: 'right', - size: 'small', - submitButtonLabel: webhook ? 'Save' : 'Create', - view: EditOverlay, - events: this.events, - contentType: webhook ? webhook.contentType : null, - webhook: webhook ? { - contentType: webhook.contentType ? webhook.contentType.id : null, - enabled: webhook.enabled, - event: webhook.event.id, - id: webhook.id, - url: webhook.url, - } : {enabled: true}, - submit: (model) => { - model.disableSubmitButton = true; - model.submitButtonState = 'busy'; - if (!model.webhook.url) { - //Due to validation url will only be populated if it's valid, hence we can make do with checking url is there - handleSubmissionError(model, 'Please provide a valid URL. Did you include https:// ?'); - return; - } - if (!model.webhook.event) { - handleSubmissionError(model, 'Please provide the event for which the webhook should trigger'); - return; + function loadNavigation() { + + var labels = ["treeHeaders_webhooks", "webhooks_logs"]; + + localizationService.localizeMany(labels).then(function (data) { + vm.page.labels.webhooks = data[0]; + vm.page.labels.logs = data[1]; + + vm.page.navigation = [ + { + "name": vm.page.labels.webhooks, + "icon": "icon-directions-alt", + "view": "views/webhooks/webhooks.html", + "active": webhookUri === 'overview', + "alias": "umbWebhooks", + "action": function () { + $location.path("/settings/webhooks/overview"); + } + }, + { + "name": vm.page.labels.logs, + "icon": "icon-box-alt", + "view": "views/webhooks/logs.html", + "active": webhookUri === 'logs', + "alias": "umbWebhookLogs", + "action": function () { + $location.path("/settings/webhooks/overview"); + } } - headlessWebhooksResource.save(model.webhook) - .then(() => { - this.goToPage(1); - - notificationsService.success('Webhook saved.'); - editorService.close(); - }, x => { - let errorMessage = undefined; - if (x.data.ModelState) { - errorMessage = `Message: ${Object.values(x.data.ModelState).flat().join(' ')}`; - } - handleSubmissionError(model, `Error saving webhook. ${errorMessage ?? ''}`); - }); - }, - close: () => { - editorService.close(); - }, + ]; }); } - function loadWebhooks(){ - return webhooksResource - .getAll(vm.pagination.pageNumber, vm.pagination.pageSize) - .then((result) => { - vm.webhooks = result; - - vm.pagination.pageNumber = result.pageNumber; - vm.pagination.totalItems = result.totalItems; - vm.pagination.totalPages = result.totalPages; - }); + function setPageName() { + localizationService.localize("treeHeaders_webhooks").then(function (data) { + vm.page.name = data; + }) } - - function deleteWebhook (webhook) { - overlayService.open({ - title: 'Confirm delete webhook', - content: 'Are you sure you want to delete the webhook?', - submitButtonLabel: 'Yes, delete', - submitButtonStyle: 'danger', - closeButtonLabel: 'Cancel', - submit: () => { - webhooksResource.deleteById(webhook.id) - .then(() => { - const index = this.webhooks.indexOf(webhook); - this.webhooks.splice(index, 1); - - notificationsService.success('Webhook deleted.'); - overlayService.close(); - }, () => { - notificationsService.error('Error deleting webhook.'); - }); - }, - close: () => { - overlayService.close(); - }, - }); - }; - - this.previousPage = () => this.goToPage(this.pagination.pageNumber - 1); - this.nextPage = () => this.goToPage(this.pagination.pageNumber + 1); - - this.goToPage = (pageNumber) => { - this.pagination.pageNumber = pageNumber; - this.loading = true; - loadWebhooks().then(() => { - this.loading = false; - }); - }; - - $q.all([ - promises, - loadWebhooks(), - loadEvents(), - ]).then(() => { - vm.loading = false; - }); } angular.module("umbraco").controller("Umbraco.Editors.Webhooks.OverviewController", OverviewController); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html index 2c13ec2940db..2576fa1e8b34 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html @@ -6,64 +6,15 @@ name-locked="true" hide-icon="true" hide-description="true" - hide-alias="true"> + hide-alias="true" + navigation="vm.page.navigation" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
EnabledEventUrlTypes
- - - - - {{ webhook.event }} - - {{ webhook.url }} - - Dummy content type name - - - -
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js new file mode 100644 index 000000000000..8c46c26873b8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js @@ -0,0 +1,156 @@ +(function () { + "use strict"; + + function WebhookController($q, webhooksResource, notificationsService, editorService, overlayService) { + var vm = this; + + vm.openWebhookOverlay = openWebhookOverlay; + vm.deleteWebhook = deleteWebhook; + vm.handleSubmissionError = handleSubmissionError; + + vm.pagination = { + pageNumber: 1, + pageSize: 25 + }; + + vm.page = {}; + vm.webhooks = []; + vm.events = []; + + + + function loadEvents (){ + return webhooksResource.getAllEvents() + .then((data) => { + vm.events = data; + }); + } + + function handleSubmissionError (model, errorMessage) { + notificationsService.error(errorMessage); + model.disableSubmitButton = false; + model.submitButtonState = 'error'; + } + + function openWebhookOverlay (webhook) { + let isCreating = !webhook; + editorService.open({ + title: webhook ? 'Edit webhook' : 'Add webhook', + position: 'right', + size: 'small', + submitButtonLabel: webhook ? 'Save' : 'Create', + view: "./overlays/edit.html", + events: vm.events, + contentType: webhook ? webhook.contentType : null, + webhook: webhook ? { + contentType: webhook.contentType ? webhook.contentType.id : null, + enabled: webhook.enabled, + event: webhook.event.id, + id: webhook.id, + url: webhook.url + } : {enabled: true}, + submit: (model) => { + model.disableSubmitButton = true; + model.submitButtonState = 'busy'; + if (!model.webhook.url) { + //Due to validation url will only be populated if it's valid, hence we can make do with checking url is there + handleSubmissionError(model, 'Please provide a valid URL. Did you include https:// ?'); + return; + } + if (!model.webhook.event) { + handleSubmissionError(model, 'Please provide the event for which the webhook should trigger'); + return; + } + if(isCreating){ + webhooksResource.create(model.webhook) + .then(() => { + this.goToPage(1); + + notificationsService.success('Webhook saved.'); + editorService.close(); + }, x => { + let errorMessage = undefined; + if (x.data.ModelState) { + errorMessage = `Message: ${Object.values(x.data.ModelState).flat().join(' ')}`; + } + handleSubmissionError(model, `Error saving webhook. ${errorMessage ?? ''}`); + }); + } + else{ + webhooksResource.update(model.webhook) + .then(() => { + this.goToPage(1); + + notificationsService.success('Webhook saved.'); + editorService.close(); + }, x => { + let errorMessage = undefined; + if (x.data.ModelState) { + errorMessage = `Message: ${Object.values(x.data.ModelState).flat().join(' ')}`; + } + handleSubmissionError(model, `Error saving webhook. ${errorMessage ?? ''}`); + }); + } + + }, + close: () => { + editorService.close(); + } + }); + } + + function loadWebhooks(){ + console.log("Loading webhooks!") + const webhooks = webhooksResource + .getAll(vm.pagination.pageNumber, vm.pagination.pageSize) + .then((result) => { + vm.webhooks = result; + + vm.pagination.pageNumber = result.pageNumber; + vm.pagination.totalItems = result.totalItems; + vm.pagination.totalPages = result.totalPages; + }); + console.log(webhooks) + return webhooks; + } + + function deleteWebhook (webhook) { + overlayService.open({ + title: 'Confirm delete webhook', + content: 'Are you sure you want to delete the webhook?', + submitButtonLabel: 'Yes, delete', + submitButtonStyle: 'danger', + closeButtonLabel: 'Cancel', + submit: () => { + webhooksResource.deleteById(webhook.id) + .then(() => { + const index = this.webhooks.indexOf(webhook); + this.webhooks.splice(index, 1); + + notificationsService.success('Webhook deleted.'); + overlayService.close(); + }, () => { + notificationsService.error('Error deleting webhook.'); + }); + }, + close: () => { + overlayService.close(); + } + }); + } + + vm.previousPage = () => goToPage(vm.pagination.pageNumber - 1); + vm.nextPage = () => goToPage(vm.pagination.pageNumber + 1); + + vm.goToPage = (pageNumber) => { + vm.pagination.pageNumber = pageNumber; + loadWebhooks() + }; + + loadWebhooks() + loadEvents() + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.WebhookController", WebhookController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html new file mode 100644 index 000000000000..a0fd0ea1e569 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html @@ -0,0 +1,57 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EnabledEventUrlTypes
+ + + + + {{ webhook.event }} + + {{ webhook.url }} + + Dummy content type name + + + +
+ +
From 7e349395ba68f28d85451ce567289fb228134966 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:10:17 +0200 Subject: [PATCH 024/102] Enable create overlay --- .../src/views/webhooks/overlays/edit.controller.js | 4 ++-- .../src/views/webhooks/overlays/edit.html | 2 +- .../src/views/webhooks/webhooks.controller.js | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js index fab54bc2cb22..9760ba4bbf69 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js @@ -27,7 +27,7 @@ this.eventChanged = (newValue, oldValue) => { if (oldValue && newValue) { - if (oldValue.split('.')[0] !== newValue.split('.')[0]) { + if (oldValue.split !== newValue) { this.clearContentType(); } } @@ -50,4 +50,4 @@ } angular.module("umbraco").controller("Umbraco.Editors.Webhooks.EditController", EditController); -}); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html index d8574a5fb096..5f98a38869a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html @@ -27,7 +27,7 @@ - - - + + + + + Add + @@ -54,13 +59,13 @@ on-remove="vm.clearContentType(contentType.key)"> Add - Please select an event first. + Please select an event first. diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js index a5c4deb936b7..b5aaf37f60e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js @@ -8,11 +8,13 @@ vm.deleteWebhook = deleteWebhook; vm.handleSubmissionError = handleSubmissionError; vm.resolveTypeNames = resolveTypeNames; + vm.resolveEventNames = resolveEventNames; vm.page = {}; vm.webhooks = []; vm.events = []; vm.webHooksContentTypes = {}; + vm.webhookEvents = {}; function loadEvents (){ return webhooksResource.getAllEvents() @@ -21,12 +23,20 @@ }); } + function resolveEventNames(webhook) { + webhook.events.forEach((event) => { + if (!vm.webhookEvents[webhook.key]) { + vm.webhookEvents[webhook.key] = event; + } else { + vm.webhookEvents[webhook.key] += ", " + event; + } + }); + } + function resolveTypeNames(webhook) { - const isContent = webhook.event.toLowerCase().includes("content"); + const isContent = webhook.events[0].toLowerCase().includes("content"); const resource = isContent ? contentTypeResource : mediaTypeResource; - - webhook.entityKeys.forEach((key) => { if (vm.webHooksContentTypes[webhook.key]){ delete vm.webHooksContentTypes[webhook.key]; @@ -61,7 +71,7 @@ webhook: webhook ? { entityKey: webhook.contentType ? webhook.contentType.key : null, enabled: webhook.enabled, - event: webhook.event, + events: webhook.events, key: webhook.key, url: webhook.url } : {enabled: true}, @@ -73,7 +83,7 @@ handleSubmissionError(model, 'Please provide a valid URL. Did you include https:// ?'); return; } - if (!model.webhook.event) { + if (!model.webhook.events || model.webhook.events.length === 0) { handleSubmissionError(model, 'Please provide the event for which the webhook should trigger'); return; } @@ -121,6 +131,7 @@ vm.webhooks.forEach((webhook) => { resolveTypeNames(webhook); + resolveEventNames(webhook); }) }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html index 007d399bec01..241a4a2015a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html @@ -19,7 +19,7 @@ Enabled - Event + Events Url Types @@ -33,8 +33,8 @@ - - {{ webhook.event }} + + {{ vm.webhookEvents[webhook.key] }} {{ webhook.url }} From 85b39d2ac1f14ef9bcb1a884ff2c0381e440103f Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 19 Sep 2023 15:23:31 +0200 Subject: [PATCH 039/102] Fix updating current items --- .../webhooks/overlays/edit.controller.js | 32 +++++++++++++------ .../src/views/webhooks/webhooks.controller.js | 29 ++++++++++++++--- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js index f1bcacb096c7..6b07331cf26e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js @@ -1,8 +1,9 @@ (function () { "use strict"; - function EditController($scope, editorService) { + function EditController($scope, editorService, contentTypeResource, mediaTypeResource) { var vm = this; vm.clearContentType = clearContentType; + vm.clearEvent = clearEvent; this.openEventPicker = () => { editorService.eventPicker({ title: "Select event", @@ -24,7 +25,7 @@ entityType: isContent ? 'DocumentType' : 'MediaType', multiPicker: true, submit(model) { - $scope.model.contentTypes = model.selection; + getEntities(model.selection, isContent); $scope.model.webhook.entityKeys = model.selection.map((item) => item.key); editorService.close(); }, @@ -34,6 +35,18 @@ }); }; + function getEntities(selection, isContent) { + const resource = isContent ? contentTypeResource : mediaTypeResource; + $scope.model.contentTypes = []; + + selection.forEach((entity) => { + resource.getById(entity.key) + .then((data) => { + $scope.model.contentTypes.push(data); + }); + }); + } + function clearContentType (contentTypeKey) { if (Array.isArray($scope.model.webhook.entityKeys)) { @@ -44,16 +57,15 @@ } } - this.eventChanged = (newValue, oldValue) => { - if (oldValue && newValue) { - if (oldValue.split !== newValue) { - this.clearContentType(); - } + function clearEvent(event) { + if (Array.isArray($scope.model.webhook.events)) { + $scope.model.webhook.events = $scope.model.webhook.events.filter(x => x !== event); } - if (!newValue) { - this.clearContentType(); + + if (Array.isArray($scope.model.contentTypes)) { + $scope.model.events = $scope.model.events.filter(x => x.key !== event); } - }; + } this.close = () => { if ($scope.model.close) { diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js index b5aaf37f60e9..fdfcccb4bddd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js @@ -33,14 +33,30 @@ }); } + function getEntities(webhook) { + const isContent = webhook.events[0].toLowerCase().includes("content"); + const resource = isContent ? contentTypeResource : mediaTypeResource; + let entities = []; + + webhook.entityKeys.forEach((key) => { + resource.getById(key) + .then((data) => { + entities.push(data); + }); + }); + + return entities; + } + function resolveTypeNames(webhook) { const isContent = webhook.events[0].toLowerCase().includes("content"); const resource = isContent ? contentTypeResource : mediaTypeResource; + if (vm.webHooksContentTypes[webhook.key]){ + delete vm.webHooksContentTypes[webhook.key]; + } + webhook.entityKeys.forEach((key) => { - if (vm.webHooksContentTypes[webhook.key]){ - delete vm.webHooksContentTypes[webhook.key]; - } resource.getById(key) .then((data) => { if (!vm.webHooksContentTypes[webhook.key]) { @@ -67,9 +83,9 @@ submitButtonLabel: webhook ? 'Save' : 'Create', view: "views/webhooks/overlays/edit.html", events: vm.events, - contentType: webhook ? webhook.contentType : null, + contentTypes : webhook ? getEntities(webhook) : null, webhook: webhook ? { - entityKey: webhook.contentType ? webhook.contentType.key : null, + entityKeys: webhook.entityKeys, enabled: webhook.enabled, events: webhook.events, key: webhook.key, @@ -90,6 +106,7 @@ if(isCreating){ webhooksResource.create(model.webhook) .then(() => { + console.log("Loading freaking webhooks") loadWebhooks() notificationsService.success('Webhook saved.'); editorService.close(); @@ -128,6 +145,8 @@ .getAll() .then((result) => { vm.webhooks = result; + vm.webhookEvents = {}; + vm.webHooksContentTypes = {}; vm.webhooks.forEach((webhook) => { resolveTypeNames(webhook); From 3323958f7ba5de78d3e94b61c134da69096b0db2 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:07:00 +0200 Subject: [PATCH 040/102] Fix up update functionality after db rework --- .../Implement/WebhookRepository.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index d53811640920..c449fc505ff4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -62,10 +62,7 @@ protected override void PersistNewItem(Webhook entity) var id = Convert.ToInt32(Database.Insert(webhookDto)); entity.Id = id; - IEnumerable buildEntityKey2WebhookDtos = WebhookFactory.BuildEntityKey2WebhookDto(entity, id); - IEnumerable buildEvent2WebhookDtos = WebhookFactory.BuildEvent2WebhookDto(entity, id); - Database.InsertBulk(buildEntityKey2WebhookDtos); - Database.InsertBulk(buildEvent2WebhookDtos); + InsertManyToOneReferences(entity); entity.ResetDirtyProperties(); } @@ -77,6 +74,10 @@ protected override void PersistUpdatedItem(Webhook entity) WebhookDto dto = WebhookFactory.BuildDto(entity); Database.Update(dto); + // Delete and re-insert the many to one references (event & entity keys) + DeleteUpdateManyToOneReferences(dto.Id); + InsertManyToOneReferences(entity); + entity.ResetDirtyProperties(); } @@ -130,4 +131,19 @@ private Webhook DtoToEntity(WebhookDto dto) return entity; } + + private void DeleteUpdateManyToOneReferences(int webhookId) + { + Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); + Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); + } + + private void InsertManyToOneReferences(Webhook webhook) + { + IEnumerable buildEntityKey2WebhookDtos = WebhookFactory.BuildEntityKey2WebhookDto(webhook, webhook.Id); + IEnumerable buildEvent2WebhookDtos = WebhookFactory.BuildEvent2WebhookDto(webhook, webhook.Id); + + Database.InsertBulk(buildEntityKey2WebhookDtos); + Database.InsertBulk(buildEvent2WebhookDtos); + } } From 682451b184b9627dfa1e131ab7fbe0dded838fba Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:42:04 +0200 Subject: [PATCH 041/102] Add webhook icon --- src/Umbraco.Core/Constants-Icons.cs | 5 +++++ src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs | 2 +- src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg | 1 + .../src/common/services/editor.service.js | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 5cfc2808fc45..65e123f2bce9 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -158,5 +158,10 @@ public static class Icons /// System user group icon /// public const string UserGroup = "icon-users"; + + /// + /// Webhooks icon + /// + public const string Webhooks = "icon-webhook"; } } diff --git a/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs index 82cad01c7901..5f315f3cbb60 100644 --- a/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs @@ -52,7 +52,7 @@ protected override ActionResult GetMenuForNode(string id, Fo { // This will load in a custom UI instead of the dashboard for the root node root.RoutePath = $"{Constants.Applications.Settings}/{Constants.Trees.Webhooks}/overview"; - root.Icon = Constants.Icons.ListView; + root.Icon = Constants.Icons.Webhooks; root.HasChildren = false; root.MenuUrl = null; } diff --git a/src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg b/src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg new file mode 100644 index 000000000000..b7febe070573 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 512e06d7aae4..6ce2a6119712 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -1186,7 +1186,7 @@ When building a custom infinite editor view you can use the same components as a memberPicker: memberPicker, memberEditor: memberEditor, mediaCropDetails, - eventPicker : eventPicker, + eventPicker : eventPicker }; return service; From 0d4a3e7fc6301ce4cc3c7b527ae75d18f4dce363 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 20 Sep 2023 09:15:37 +0200 Subject: [PATCH 042/102] Switch to match heartcore icons --- src/Umbraco.Core/Constants-Icons.cs | 2 +- src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 65e123f2bce9..5aaeb2ba61af 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -162,6 +162,6 @@ public static class Icons /// /// Webhooks icon /// - public const string Webhooks = "icon-webhook"; + public const string Webhooks = "icon-directions-alt"; } } diff --git a/src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg b/src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg deleted file mode 100644 index b7febe070573..000000000000 --- a/src/Umbraco.Web.UI.Client/src/assets/icons/icon-webhook.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From d059c103c930dbf7ad6ed2be459c6d3ac0c8a757 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 21 Sep 2023 13:51:44 +0200 Subject: [PATCH 043/102] Refactor to use bases instead of Enum --- .../DependencyInjection/UmbracoBuilder.cs | 6 ++++ src/Umbraco.Core/Models/Webhook.cs | 13 ++++---- src/Umbraco.Core/Models/WebhookEvent.cs | 10 ------ .../Webhooks/ContentDeleteWebhookEvent.cs | 15 +++++++++ .../Webhooks/ContentPublishWebhookEvent.cs | 15 +++++++++ .../Webhooks/ContentUnpublishWebhookEvent.cs | 13 ++++++++ src/Umbraco.Core/Webhooks/IWebhookEvent.cs | 6 ++++ .../Webhooks/MediaDeleteWebhookEvent.cs | 10 ++++++ .../Webhooks/MediaSaveWebhookEvent.cs | 10 ++++++ src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 14 ++++++++ .../Persistence/Factories/WebhookFactory.cs | 5 +-- .../Controllers/WebHookController.cs | 33 ++++++++++++++++--- .../Models/WebhookViewModel.cs | 2 +- 13 files changed, 128 insertions(+), 24 deletions(-) delete mode 100644 src/Umbraco.Core/Models/WebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/IWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/WebhookEventBase.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 2c856b1a1691..e291fa7f17f1 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -40,6 +40,7 @@ using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection @@ -327,6 +328,11 @@ private void AddCoreServices() Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddNotificationAsyncHandler(); + Services.AddNotificationAsyncHandler(); + Services.AddNotificationAsyncHandler(); + Services.AddNotificationAsyncHandler(); + Services.AddNotificationAsyncHandler(); } } } diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index b5a40bb2cf0d..f305071d4a73 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -1,5 +1,4 @@ -using System.Runtime.Serialization; -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models; @@ -7,7 +6,7 @@ namespace Umbraco.Cms.Core.Models; public class Webhook : EntityBase { private string _url; - private WebhookEvent[] _events; + private string[] _events; private Guid[] _entityKeys; private bool _enabled; @@ -18,15 +17,15 @@ public class Webhook : EntityBase enum1 => enum1.GetHashCode()); // Custom comparer for enumerable webhook events - private static readonly DelegateEqualityComparer> _webhookEventEnumerableComparer = + private static readonly DelegateEqualityComparer> _webhookEventEnumerableComparer = new( (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), enum1 => enum1.GetHashCode()); - public Webhook(string url, bool? enabled = null, Guid[]? entityKeys = null, WebhookEvent[]? events = null) + public Webhook(string url, bool? enabled = null, Guid[]? entityKeys = null, string[]? events = null) { _url = url; - _events = events ?? Array.Empty(); + _events = events ?? Array.Empty(); _entityKeys = entityKeys ?? Array.Empty(); _enabled = enabled ?? false; } @@ -37,7 +36,7 @@ public string Url set => SetPropertyValueAndDetectChanges(value, ref _url!, nameof(Url)); } - public WebhookEvent[] Events + public string[] Events { get => _events; set => SetPropertyValueAndDetectChanges(value, ref _events!, nameof(Events), _webhookEventEnumerableComparer); diff --git a/src/Umbraco.Core/Models/WebhookEvent.cs b/src/Umbraco.Core/Models/WebhookEvent.cs deleted file mode 100644 index d832f0a4bcd8..000000000000 --- a/src/Umbraco.Core/Models/WebhookEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Umbraco.Cms.Core.Models; - -public enum WebhookEvent -{ - ContentPublish, - ContentUnpublish, - ContentDelete, - MediaSave, - MediaDelete, -} diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs new file mode 100644 index 000000000000..d8eaca5a94e0 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Webhooks; + +public class ContentDeleteWebhookEvent : WebhookEventBase +{ + public override string EventName => "ContentPublish"; + + public override async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) + { + // Implement your handling logic for the ContentPublish event + // You can access properties of the ContentSavedNotification here + await Task.Yield(); // Replace with your actual async logic + } +} diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs new file mode 100644 index 000000000000..494e7ace4302 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Webhooks; + +public class ContentPublishWebhookEvent : WebhookEventBase +{ + public override string EventName => "ContentPublish"; + + public override async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) + { + // Implement your handling logic for the ContentPublish event + // You can access properties of the ContentSavedNotification here + await Task.Yield(); // Replace with your actual async logic + } +} diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs new file mode 100644 index 000000000000..c6f85d80ee4c --- /dev/null +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Webhooks; + +public class ContentUnpublishWebhookEvent : WebhookEventBase +{ + public override string EventName => "ContentUnpublish"; + + public override async Task HandleAsync(ContentUnpublishedNotification notification, CancellationToken cancellationToken) + { + await Task.Yield(); // Replace with your actual async logic + } +} diff --git a/src/Umbraco.Core/Webhooks/IWebhookEvent.cs b/src/Umbraco.Core/Webhooks/IWebhookEvent.cs new file mode 100644 index 000000000000..954055d104d8 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/IWebhookEvent.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Webhooks; + +public interface IWebhookEvent +{ + string EventName { get; } +} diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs new file mode 100644 index 000000000000..761339947c99 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Webhooks; + +public class MediaDeleteWebhookEvent : WebhookEventBase +{ + public override string EventName => "MediaDelete"; + + public override Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); +} diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs new file mode 100644 index 000000000000..0595a3af7cca --- /dev/null +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Webhooks; + +public class MediaSaveWebhookEvent : WebhookEventBase +{ + public override string EventName => "MediaSave"; + + public override Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); +} diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs new file mode 100644 index 000000000000..481daedc4657 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Webhooks; + +public abstract class WebhookEventBase : IWebhookEvent, INotificationAsyncHandler + where T : INotification +{ + + public abstract string EventName { get; } + + public abstract Task HandleAsync(T notification, CancellationToken cancellationToken); +} + diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index 6de030af7230..642ee66c1bed 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Factories; @@ -11,7 +12,7 @@ public static Webhook BuildEntity(WebhookDto dto, IEnumerable x.EntityKey).ToArray(), - event2WebhookDtos?.Select(x => Enum.Parse(x.Event)).ToArray()); + event2WebhookDtos?.Select(x => x.Event).ToArray()); try { @@ -56,7 +57,7 @@ public static IEnumerable BuildEntityKey2WebhookDto(Webhoo public static IEnumerable BuildEvent2WebhookDto(Webhook webhook, int webhookId) => webhook.Events.Select(x => new Event2WebhookDto { - Event = x.ToString(), + Event = x, WebhookId = webhookId, }); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs index 22dce2773660..a2fb36b8d3c1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.Mvc; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Web.Common.Models; namespace Umbraco.Cms.Web.BackOffice.Controllers; @@ -62,8 +64,31 @@ public async Task Delete(Guid key) return Ok(); } - // TODO: This should probably be handled by the NewtonsoftJsonOutputFormatter instead [HttpGet] - public async Task GetEvents() => - Ok(Enum.GetValues(typeof(WebhookEvent)).Cast().Select(x => x.ToString())); + public IActionResult GetEvents() + { + // Load the assembly containing your webhook event classes + Assembly assembly = typeof(IWebhookEvent).Assembly; + + // Get all types in the assembly that implement IWebhookEvent + var webhookEventTypes = assembly + .GetTypes() + .Where(type => typeof(IWebhookEvent).IsAssignableFrom(type) && !type.IsAbstract); + + // Create instances of each type and select the EventName property value for each instance + var eventNames = webhookEventTypes + .Select(type => + { + if (Activator.CreateInstance(type) is IWebhookEvent webhookEvent) + { + return webhookEvent.EventName; + } + + return null; + }) + .Where(eventName => !string.IsNullOrEmpty(eventName)) + .ToArray(); + + return Ok(eventNames); + } } diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs index 58e7a9ba8a6b..820964baf988 100644 --- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -13,7 +13,7 @@ public class WebhookViewModel public string Url { get; set; } = string.Empty; [DataMember(Name = "events")] - public WebhookEvent[] Events { get; set; } = Array.Empty(); + public string[] Events { get; set; } = Array.Empty(); [DataMember(Name = "entityKeys")] public Guid[] EntityKeys { get; set; } = Array.Empty(); From c7c856208e83e9d94701e7a41d1171b695e03eea Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:49:20 +0200 Subject: [PATCH 044/102] Refactor to make IWebhookEvent to Collection, so it can be injected instead of using reflection --- .../UmbracoBuilder.Collections.cs | 13 +++++++++ .../Webhooks/ContentDeleteWebhookEvent.cs | 2 +- .../Webhooks/ContentPublishWebhookEvent.cs | 2 +- .../Webhooks/ContentUnpublishWebhookEvent.cs | 2 +- src/Umbraco.Core/Webhooks/IWebhookEvent.cs | 2 +- .../Webhooks/MediaDeleteWebhookEvent.cs | 2 +- .../Webhooks/MediaSaveWebhookEvent.cs | 2 +- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 2 +- .../Webhooks/WebhookEventCollection.cs | 10 +++++++ .../Webhooks/WebhookEventCollectionBuilder.cs | 8 +++++ .../Controllers/WebHookController.cs | 29 ++++--------------- .../Mapping/WebhookMapDefinition.cs | 5 ++++ .../Models/WebhookEventViewModel.cs | 10 +++++++ 13 files changed, 58 insertions(+), 31 deletions(-) create mode 100644 src/Umbraco.Core/Webhooks/WebhookEventCollection.cs create mode 100644 src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs create mode 100644 src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 73eb599695cf..abcaa16fee40 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Tour; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.WebAssets; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection; @@ -128,6 +129,12 @@ internal static void AddAllCoreCollectionBuilders(this IUmbracoBuilder builder) builder.FilterHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.SortHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.ContentIndexHandlers().Add(() => builder.TypeLoader.GetTypes()); + builder.WebhookEvents() + .Append() + .Append() + .Append() + .Append() + .Append(); } /// @@ -195,6 +202,12 @@ public static MediaUrlProviderCollectionBuilder MediaUrlProviders(this IUmbracoB public static SectionCollectionBuilder Sections(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + /// + /// Gets the backoffice sections/applications collection builder. + /// + /// The builder. + public static WebhookEventCollectionBuilder WebhookEvents(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + /// /// Gets the components collection builder. /// diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index d8eaca5a94e0..475745be8fe9 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentDeleteWebhookEvent : WebhookEventBase { - public override string EventName => "ContentPublish"; + public ContentDeleteWebhookEvent() => EventName = "ContentPublish"; public override async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 494e7ace4302..6ee68de3b0e5 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentPublishWebhookEvent : WebhookEventBase { - public override string EventName => "ContentPublish"; + public ContentPublishWebhookEvent() => EventName = "ContentPublish"; public override async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index c6f85d80ee4c..8eed6edf800d 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentUnpublishWebhookEvent : WebhookEventBase { - public override string EventName => "ContentUnpublish"; + public ContentUnpublishWebhookEvent() => EventName = "ContentUnpublish"; public override async Task HandleAsync(ContentUnpublishedNotification notification, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Core/Webhooks/IWebhookEvent.cs b/src/Umbraco.Core/Webhooks/IWebhookEvent.cs index 954055d104d8..85857c1aecdf 100644 --- a/src/Umbraco.Core/Webhooks/IWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/IWebhookEvent.cs @@ -2,5 +2,5 @@ public interface IWebhookEvent { - string EventName { get; } + string EventName { get; set; } } diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index 761339947c99..941a84532c98 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class MediaDeleteWebhookEvent : WebhookEventBase { - public override string EventName => "MediaDelete"; + public MediaDeleteWebhookEvent() => EventName = "MediaDelete"; public override Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index 0595a3af7cca..3bca3caf827a 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class MediaSaveWebhookEvent : WebhookEventBase { - public override string EventName => "MediaSave"; + public MediaSaveWebhookEvent() => EventName = "MediaSave"; public override Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 481daedc4657..8daa232bee7b 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -7,7 +7,7 @@ public abstract class WebhookEventBase : IWebhookEvent, INotificationAsyncHan where T : INotification { - public abstract string EventName { get; } + public string EventName { get; set; } = string.Empty; public abstract Task HandleAsync(T notification, CancellationToken cancellationToken); } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventCollection.cs b/src/Umbraco.Core/Webhooks/WebhookEventCollection.cs new file mode 100644 index 000000000000..cf939f93aeb3 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/WebhookEventCollection.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Webhooks; + +public class WebhookEventCollection : BuilderCollectionBase +{ + public WebhookEventCollection(Func> items) : base(items) + { + } +} diff --git a/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs b/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs new file mode 100644 index 000000000000..87b9da8b45bf --- /dev/null +++ b/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Webhooks; + +public class WebhookEventCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override WebhookEventCollectionBuilder This => this; +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs index a2fb36b8d3c1..155833e55da5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -12,11 +12,13 @@ public class WebHookController : UmbracoAuthorizedJsonController { private readonly IWebHookService _webHookService; private readonly IUmbracoMapper _umbracoMapper; + private readonly WebhookEventCollection _webhookEventCollection; - public WebHookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper) + public WebHookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection) { _webHookService = webHookService; _umbracoMapper = umbracoMapper; + _webhookEventCollection = webhookEventCollection; } [HttpGet] @@ -67,28 +69,7 @@ public async Task Delete(Guid key) [HttpGet] public IActionResult GetEvents() { - // Load the assembly containing your webhook event classes - Assembly assembly = typeof(IWebhookEvent).Assembly; - - // Get all types in the assembly that implement IWebhookEvent - var webhookEventTypes = assembly - .GetTypes() - .Where(type => typeof(IWebhookEvent).IsAssignableFrom(type) && !type.IsAbstract); - - // Create instances of each type and select the EventName property value for each instance - var eventNames = webhookEventTypes - .Select(type => - { - if (Activator.CreateInstance(type) is IWebhookEvent webhookEvent) - { - return webhookEvent.EventName; - } - - return null; - }) - .Where(eventName => !string.IsNullOrEmpty(eventName)) - .ToArray(); - - return Ok(eventNames); + List viewModels = _umbracoMapper.MapEnumerable(_webhookEventCollection.AsEnumerable()); + return Ok(viewModels); } } diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index 30e58aee98c9..afd61df1f998 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -1,5 +1,6 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Web.Common.Models; namespace Umbraco.Cms.Web.BackOffice.Mapping; @@ -10,6 +11,7 @@ public void DefineMaps(IUmbracoMapper mapper) { mapper.Define((_, _) => new Webhook(string.Empty), Map); mapper.Define((_, _) => new WebhookViewModel(), Map); + mapper.Define((_, _) => new WebhookEventViewModel(), Map); } // Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -Key -UpdateDate @@ -31,4 +33,7 @@ private void Map(Webhook source, WebhookViewModel target, MapperContext context) target.Enabled = source.Enabled; target.Key = source.Key; } + + // Umbraco.Code.MapAll + private void Map(IWebhookEvent source, WebhookEventViewModel target, MapperContext context) => target.EventName = source.EventName; } diff --git a/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs new file mode 100644 index 000000000000..441a3674293a --- /dev/null +++ b/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Web.Common.Models; + +[DataContract] +public class WebhookEventViewModel +{ + [DataMember(Name = "eventName")] + public string EventName { get; set; } = string.Empty; +} From 6ba193f4e9f33144a8045d925bef1ce7d48c2324 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:56:39 +0200 Subject: [PATCH 045/102] Fix up frontend to match new models --- .../infiniteeditors/eventpicker/eventpicker.controller.js | 2 +- .../src/views/webhooks/overlays/edit.controller.js | 1 - .../src/views/webhooks/overview.controller.js | 2 +- .../src/views/webhooks/webhooks.controller.js | 4 +++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.controller.js index 31043aa5749a..775b70b63d1d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.controller.js @@ -37,7 +37,7 @@ webhooksResource.getAllEvents() .then((data) => { data.forEach(function (event) { - let eventObject = { name: event, selected: false} + let eventObject = { name: event.eventName, selected: false} vm.events.push(eventObject); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js index 6b07331cf26e..5ef198d84026 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js @@ -8,7 +8,6 @@ editorService.eventPicker({ title: "Select event", submit(model) { - $scope.model.webhook.events = model.selection; editorService.close(); }, diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js index 103dfd71aa49..0dc8ebf5a8c5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function OverviewController($q, $location, $routeParams, webhooksResource, notificationsService, editorService, overlayService, localizationService) { + function OverviewController($q, $location, $routeParams, notificationsService, editorService, overlayService, localizationService) { var vm = this; vm.page = {}; vm.page.labels = {}; diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js index fdfcccb4bddd..1e2f8f5faefb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js @@ -19,7 +19,9 @@ function loadEvents (){ return webhooksResource.getAllEvents() .then((data) => { - vm.events = data; + vm.events = data.map(item => item.eventName); + console.log("logging vm.events") + console.log(vm.events) }); } From 05c395dcd615a3cd9b4de12278836ca5245bc137 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Fri, 22 Sep 2023 09:23:28 +0200 Subject: [PATCH 046/102] Fix integration tests --- .../Services/WebhookServiceTests.cs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index 927a481951c2..12b2eeef38d8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -13,12 +13,12 @@ public class WebhookServiceTests : UmbracoIntegrationTest private IWebHookService WebhookService => GetRequiredService(); [Test] - [TestCase("https://example.com", WebhookEvent.ContentPublish, "00000000-0000-0000-0000-010000000000")] - [TestCase("https://example.com", WebhookEvent.ContentDelete, "00000000-0000-0000-0000-000200000000")] - [TestCase("https://example.com", WebhookEvent.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] - [TestCase("https://example.com", WebhookEvent.MediaDelete, "00000000-0000-0000-0000-000004000000")] - [TestCase("https://example.com", WebhookEvent.MediaSave, "00000000-0000-0000-0000-000000500000")] - public async Task Can_Create_And_Get(string url, WebhookEvent webhookEvent, Guid key) + [TestCase("https://example.com", "ContentPublish", "00000000-0000-0000-0000-010000000000")] + [TestCase("https://example.com", "ContentDelete", "00000000-0000-0000-0000-000200000000")] + [TestCase("https://example.com", "ContentUnpublish", "00000000-0000-0000-0000-300000000000")] + [TestCase("https://example.com", "MediaDelete", "00000000-0000-0000-0000-000004000000")] + [TestCase("https://example.com", "MediaSave", "00000000-0000-0000-0000-000000500000")] + public async Task Can_Create_And_Get(string url, string webhookEvent, Guid key) { var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, true, new[] { key }, new[] { webhookEvent })); var webhook = await WebhookService.GetAsync(createdWebhook.Key); @@ -36,8 +36,8 @@ public async Task Can_Create_And_Get(string url, WebhookEvent webhookEvent, Guid [Test] public async Task Can_Get_Multiple() { - var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { WebhookEvent.ContentPublish })); - var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { WebhookEvent.ContentDelete })); + var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentPublish" })); + var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentDelete" })); var keys = new List { createdWebhookOne.Key, createdWebhookTwo.Key }; var webhooks = await WebhookService.GetMultipleAsync(keys); @@ -52,9 +52,9 @@ public async Task Can_Get_Multiple() [Test] public async Task Can_Get_All() { - var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { WebhookEvent.ContentPublish })); - var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { WebhookEvent.ContentDelete })); - var createdWebhookThree = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { WebhookEvent.ContentUnpublish })); + var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentPublish" })); + var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentDelete" })); + var createdWebhookThree = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentUnpublish" })); var webhooks = await WebhookService.GetAllAsync(); Assert.Multiple(() => @@ -67,12 +67,12 @@ public async Task Can_Get_All() } [Test] - [TestCase("https://example.com", WebhookEvent.ContentPublish, "00000000-0000-0000-0000-010000000000")] - [TestCase("https://example.com", WebhookEvent.ContentDelete, "00000000-0000-0000-0000-000200000000")] - [TestCase("https://example.com", WebhookEvent.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] - [TestCase("https://example.com", WebhookEvent.MediaDelete, "00000000-0000-0000-0000-000004000000")] - [TestCase("https://example.com", WebhookEvent.MediaSave, "00000000-0000-0000-0000-000000500000")] - public async Task Can_Delete(string url, WebhookEvent webhookEvent, Guid key) + [TestCase("https://example.com", "ContentPublish", "00000000-0000-0000-0000-010000000000")] + [TestCase("https://example.com", "ContentDelete", "00000000-0000-0000-0000-000200000000")] + [TestCase("https://example.com", "ContentUnpublish", "00000000-0000-0000-0000-300000000000")] + [TestCase("https://example.com", "MediaDelete", "00000000-0000-0000-0000-000004000000")] + [TestCase("https://example.com", "MediaSave", "00000000-0000-0000-0000-000000500000")] + public async Task Can_Delete(string url, string webhookEvent, Guid key) { var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, true, new[] { key }, new[] { webhookEvent })); var webhook = await WebhookService.GetAsync(createdWebhook.Key); @@ -86,7 +86,7 @@ public async Task Can_Delete(string url, WebhookEvent webhookEvent, Guid key) [Test] public async Task Can_Create_With_No_EntityKeys() { - var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { WebhookEvent.ContentPublish })); + var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { "ContentPublish" })); var webhook = await WebhookService.GetAsync(createdWebhook.Key); Assert.IsNotNull(webhook); @@ -96,13 +96,13 @@ public async Task Can_Create_With_No_EntityKeys() [Test] public async Task Can_Update() { - var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { WebhookEvent.ContentPublish })); - createdWebhook.Events = new[] { WebhookEvent.ContentDelete }; + var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { "ContentPublish" })); + createdWebhook.Events = new[] { "ContentDelete" }; await WebhookService.UpdateAsync(createdWebhook); var updatedWebhook = await WebhookService.GetAsync(createdWebhook.Key); Assert.IsNotNull(updatedWebhook); Assert.AreEqual(1, updatedWebhook.Events.Length); - Assert.IsTrue(updatedWebhook.Events.Contains(WebhookEvent.ContentDelete)); + Assert.IsTrue(updatedWebhook.Events.Contains("ContentDelete")); } } From 8be6258c5d2e36205247471f3194112d9ceecd1a Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Fri, 22 Sep 2023 10:53:38 +0200 Subject: [PATCH 047/102] Remove obsolete entity key from webhookdto --- src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs index 21145616c52c..381cd113375f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -22,9 +22,6 @@ internal class WebhookDto [NullSetting(NullSetting = NullSettings.NotNull)] public string Url { get; set; } = string.Empty; - [Column(Name = "entityKey")] - public Guid EntityKey { get; set; } - [Column(Name = "enabled")] public bool Enabled { get; set; } } From 65ba2a597fbadba9275f6c30bcb128be42e51420 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 25 Sep 2023 12:40:41 +0200 Subject: [PATCH 048/102] Introduce constants instead of hard coded strings --- src/Umbraco.Core/Constants-WebhookEvents.cs | 32 +++++++++++++++ .../Webhooks/ContentDeleteWebhookEvent.cs | 2 +- .../Webhooks/ContentPublishWebhookEvent.cs | 2 +- .../Webhooks/ContentUnpublishWebhookEvent.cs | 2 +- .../Webhooks/MediaDeleteWebhookEvent.cs | 2 +- .../Webhooks/MediaSaveWebhookEvent.cs | 2 +- .../Services/WebhookServiceTests.cs | 39 ++++++++++--------- 7 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 src/Umbraco.Core/Constants-WebhookEvents.cs diff --git a/src/Umbraco.Core/Constants-WebhookEvents.cs b/src/Umbraco.Core/Constants-WebhookEvents.cs new file mode 100644 index 000000000000..dbe48e7babe3 --- /dev/null +++ b/src/Umbraco.Core/Constants-WebhookEvents.cs @@ -0,0 +1,32 @@ +namespace Umbraco.Cms.Core; + +public static partial class Constants +{ + public static class WebhookEvents + { + /// + /// Webhook event name for content publish. + /// + public const string ContentPublish = "ContentPublish"; + + /// + /// Webhook event name for content delete. + /// + public const string ContentDelete = "ContentDelete"; + + /// + /// Webhook event name for content unpublish. + /// + public const string ContentUnpublish = "ContentUnpublish"; + + /// + /// Webhook event name for media delete. + /// + public const string MediaDelete = "MediaDelete"; + + /// + /// Webhook event name for media save. + /// + public const string MediaSave = "MediaDelete"; + } +} diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index 475745be8fe9..1990025e812e 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentDeleteWebhookEvent : WebhookEventBase { - public ContentDeleteWebhookEvent() => EventName = "ContentPublish"; + public ContentDeleteWebhookEvent() => EventName = Constants.WebhookEvents.ContentDelete; public override async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 6ee68de3b0e5..da2b802cddca 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentPublishWebhookEvent : WebhookEventBase { - public ContentPublishWebhookEvent() => EventName = "ContentPublish"; + public ContentPublishWebhookEvent() => EventName = Constants.WebhookEvents.ContentPublish; public override async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index 8eed6edf800d..c553e8a9ca7f 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentUnpublishWebhookEvent : WebhookEventBase { - public ContentUnpublishWebhookEvent() => EventName = "ContentUnpublish"; + public ContentUnpublishWebhookEvent() => EventName = Constants.WebhookEvents.ContentUnpublish; public override async Task HandleAsync(ContentUnpublishedNotification notification, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index 941a84532c98..a49cfaab78ae 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class MediaDeleteWebhookEvent : WebhookEventBase { - public MediaDeleteWebhookEvent() => EventName = "MediaDelete"; + public MediaDeleteWebhookEvent() => EventName = Constants.WebhookEvents.MediaDelete; public override Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index 3bca3caf827a..aa4fe37caeb7 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Core.Webhooks; public class MediaSaveWebhookEvent : WebhookEventBase { - public MediaSaveWebhookEvent() => EventName = "MediaSave"; + public MediaSaveWebhookEvent() => EventName = Constants.WebhookEvents.MediaSave; public override Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index 12b2eeef38d8..34dc96e527d7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Testing; @@ -13,11 +14,11 @@ public class WebhookServiceTests : UmbracoIntegrationTest private IWebHookService WebhookService => GetRequiredService(); [Test] - [TestCase("https://example.com", "ContentPublish", "00000000-0000-0000-0000-010000000000")] - [TestCase("https://example.com", "ContentDelete", "00000000-0000-0000-0000-000200000000")] - [TestCase("https://example.com", "ContentUnpublish", "00000000-0000-0000-0000-300000000000")] - [TestCase("https://example.com", "MediaDelete", "00000000-0000-0000-0000-000004000000")] - [TestCase("https://example.com", "MediaSave", "00000000-0000-0000-0000-000000500000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentPublish, "00000000-0000-0000-0000-010000000000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentDelete, "00000000-0000-0000-0000-000200000000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] + [TestCase("https://example.com", Constants.WebhookEvents.MediaDelete, "00000000-0000-0000-0000-000004000000")] + [TestCase("https://example.com", Constants.WebhookEvents.MediaSave, "00000000-0000-0000-0000-000000500000")] public async Task Can_Create_And_Get(string url, string webhookEvent, Guid key) { var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, true, new[] { key }, new[] { webhookEvent })); @@ -36,8 +37,8 @@ public async Task Can_Create_And_Get(string url, string webhookEvent, Guid key) [Test] public async Task Can_Get_Multiple() { - var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentPublish" })); - var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentDelete" })); + var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentPublish })); + var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentDelete })); var keys = new List { createdWebhookOne.Key, createdWebhookTwo.Key }; var webhooks = await WebhookService.GetMultipleAsync(keys); @@ -52,9 +53,9 @@ public async Task Can_Get_Multiple() [Test] public async Task Can_Get_All() { - var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentPublish" })); - var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentDelete" })); - var createdWebhookThree = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { "ContentUnpublish" })); + var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentPublish })); + var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentDelete })); + var createdWebhookThree = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentUnpublish })); var webhooks = await WebhookService.GetAllAsync(); Assert.Multiple(() => @@ -67,11 +68,11 @@ public async Task Can_Get_All() } [Test] - [TestCase("https://example.com", "ContentPublish", "00000000-0000-0000-0000-010000000000")] - [TestCase("https://example.com", "ContentDelete", "00000000-0000-0000-0000-000200000000")] - [TestCase("https://example.com", "ContentUnpublish", "00000000-0000-0000-0000-300000000000")] - [TestCase("https://example.com", "MediaDelete", "00000000-0000-0000-0000-000004000000")] - [TestCase("https://example.com", "MediaSave", "00000000-0000-0000-0000-000000500000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentPublish, "00000000-0000-0000-0000-010000000000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentDelete, "00000000-0000-0000-0000-000200000000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] + [TestCase("https://example.com", Constants.WebhookEvents.MediaDelete, "00000000-0000-0000-0000-000004000000")] + [TestCase("https://example.com", Constants.WebhookEvents.MediaSave, "00000000-0000-0000-0000-000000500000")] public async Task Can_Delete(string url, string webhookEvent, Guid key) { var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, true, new[] { key }, new[] { webhookEvent })); @@ -86,7 +87,7 @@ public async Task Can_Delete(string url, string webhookEvent, Guid key) [Test] public async Task Can_Create_With_No_EntityKeys() { - var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { "ContentPublish" })); + var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentPublish })); var webhook = await WebhookService.GetAsync(createdWebhook.Key); Assert.IsNotNull(webhook); @@ -96,13 +97,13 @@ public async Task Can_Create_With_No_EntityKeys() [Test] public async Task Can_Update() { - var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { "ContentPublish" })); - createdWebhook.Events = new[] { "ContentDelete" }; + var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentPublish })); + createdWebhook.Events = new[] { Constants.WebhookEvents.ContentDelete }; await WebhookService.UpdateAsync(createdWebhook); var updatedWebhook = await WebhookService.GetAsync(createdWebhook.Key); Assert.IsNotNull(updatedWebhook); Assert.AreEqual(1, updatedWebhook.Events.Length); - Assert.IsTrue(updatedWebhook.Events.Contains("ContentDelete")); + Assert.IsTrue(updatedWebhook.Events.Contains(Constants.WebhookEvents.ContentDelete)); } } From 9800145c4280782391c2c76cb38559af01069b90 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:45:12 +0200 Subject: [PATCH 049/102] Start implementation of firing mechanism --- src/Umbraco.Core/Webhooks/IRetryService.cs | 6 ++++ .../Webhooks/IWebhookFiringService.cs | 6 ++++ .../UmbracoBuilder.CoreServices.cs | 4 +++ .../Services/Implement/RetryService.cs | 28 +++++++++++++++++ .../Implement/WebhookFiringService.cs | 30 +++++++++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 src/Umbraco.Core/Webhooks/IRetryService.cs create mode 100644 src/Umbraco.Core/Webhooks/IWebhookFiringService.cs create mode 100644 src/Umbraco.Infrastructure/Services/Implement/RetryService.cs create mode 100644 src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs diff --git a/src/Umbraco.Core/Webhooks/IRetryService.cs b/src/Umbraco.Core/Webhooks/IRetryService.cs new file mode 100644 index 000000000000..56e0ebe0e1f8 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/IRetryService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Webhooks; + +public interface IRetryService +{ + Task RetryAsync(Func> action, int maxRetries = 3, TimeSpan? retryDelay = null); +} diff --git a/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs b/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs new file mode 100644 index 000000000000..5238c4430a8f --- /dev/null +++ b/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Webhooks; + +public interface IWebhookFiringService +{ + Task Fire( string url, object? requestObject); +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 541631056690..8ac7f545a5e7 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -38,6 +38,7 @@ using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Infrastructure.DeliveryApi; using Umbraco.Cms.Infrastructure.DistributedLocking; using Umbraco.Cms.Infrastructure.Examine; @@ -226,6 +227,9 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde builder.AddDeliveryApiCoreServices(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; } diff --git a/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs b/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs new file mode 100644 index 000000000000..868798b9fd9d --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs @@ -0,0 +1,28 @@ +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Infrastructure.Services.Implement; + +public class RetryService : IRetryService +{ + public async Task RetryAsync(Func> action, int maxRetries = 3, TimeSpan? retryDelay = null) + { + for (int retry = 0; retry < maxRetries; retry++) + { + try + { + return await action(); + } + catch (Exception ex) + { + // Retry after a delay, if specified + if (retryDelay != null) + { + await Task.Delay(retryDelay.Value); + } + } + } + + return null!; + // TODO: Every retry failed, should we log some errors here, maybe the error in the catch? + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs new file mode 100644 index 000000000000..74a902ca23d1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs @@ -0,0 +1,30 @@ +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Infrastructure.Services.Implement; + +public class WebhookFiringService : IWebhookFiringService +{ + private readonly IJsonSerializer _jsonSerializer; + private readonly IRetryService _retryService; + private readonly int _maxRetries = 3; + + public WebhookFiringService(IJsonSerializer jsonSerializer, IRetryService retryService) + { + _jsonSerializer = jsonSerializer; + _retryService = retryService; + } + + public async Task Fire(string url, object? requestObject) => await _retryService.RetryAsync( + async () => + { + using var httpClient = new HttpClient(); + + var myContent = _jsonSerializer.Serialize(requestObject); + var buffer = System.Text.Encoding.UTF8.GetBytes(myContent); + var byteContent = new ByteArrayContent(buffer); + + return await httpClient.PostAsync(url, byteContent); + }, + _maxRetries); +} From bd52ba852bdb55c4671c949df67f739872222003 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:42:20 +0200 Subject: [PATCH 050/102] Add new GetByEventName method --- src/Umbraco.Core/Services/IWebHookService.cs | 2 + src/Umbraco.Core/Services/WebhookService.cs | 32 ++++++++++------ .../Webhooks/ContentPublishWebhookEvent.cs | 38 ++++++++++++++++--- .../Services/WebhookServiceTests.cs | 13 +++++++ 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs index b4d345d233d9..49fe0ce68778 100644 --- a/src/Umbraco.Core/Services/IWebHookService.cs +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -14,4 +14,6 @@ public interface IWebHookService Task> GetMultipleAsync(IEnumerable keys); Task> GetAllAsync(); + + Task> GetByEventName(string eventName); } diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 54be5a7eba34..1639312ba76f 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -1,23 +1,24 @@ -using Umbraco.Cms.Core.Models; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services; -public class WebhookService : IWebHookService +public class WebhookService : RepositoryService, IWebHookService { private readonly IWebhookRepository _webhookRepository; - private readonly ICoreScopeProvider _coreScopeProvider; - public WebhookService(IWebhookRepository webhookRepository, ICoreScopeProvider coreScopeProvider) + public WebhookService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IWebhookRepository webhookRepository) : base(provider, loggerFactory, eventMessagesFactory) { _webhookRepository = webhookRepository; - _coreScopeProvider = coreScopeProvider; } public Task CreateAsync(Webhook webhook) { - using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + using ICoreScope scope = ScopeProvider.CreateCoreScope(); _webhookRepository.Save(webhook); scope.Complete(); @@ -26,7 +27,7 @@ public Task CreateAsync(Webhook webhook) public Task UpdateAsync(Webhook updateModel) { - using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + using ICoreScope scope = ScopeProvider.CreateCoreScope(); // TODO: Validation if we need it @@ -50,7 +51,7 @@ public Task UpdateAsync(Webhook updateModel) public Task DeleteAsync(Guid key) { - using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + using ICoreScope scope = ScopeProvider.CreateCoreScope(); Webhook? webhook = _webhookRepository.Get(key); if (webhook is not null) { @@ -63,7 +64,7 @@ public Task DeleteAsync(Guid key) public Task GetAsync(Guid key) { - using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + using ICoreScope scope = ScopeProvider.CreateCoreScope(); Webhook? webhook = _webhookRepository.Get(key); scope.Complete(); return Task.FromResult(webhook); @@ -71,7 +72,7 @@ public Task DeleteAsync(Guid key) public Task> GetMultipleAsync(IEnumerable keys) { - using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + using ICoreScope scope = ScopeProvider.CreateCoreScope(); IEnumerable webhooks = _webhookRepository.GetMany(keys.ToArray()); scope.Complete(); return Task.FromResult(webhooks); @@ -79,9 +80,18 @@ public Task> GetMultipleAsync(IEnumerable keys) public Task> GetAllAsync() { - using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + using ICoreScope scope = ScopeProvider.CreateCoreScope(); IEnumerable webhooks = _webhookRepository.GetMany(); scope.Complete(); return Task.FromResult(webhooks); } + + public Task> GetByEventName(string eventName) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IQuery query = Query().Where(x => x.Events.Contains(eventName)); + IEnumerable webhooks = _webhookRepository.Get(query); + scope.Complete(); + return Task.FromResult(webhooks); + } } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index da2b802cddca..88cc74a4637c 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -1,15 +1,43 @@ -using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentPublishWebhookEvent : WebhookEventBase { - public ContentPublishWebhookEvent() => EventName = Constants.WebhookEvents.ContentPublish; + private readonly IWebhookFiringService _webhookFiringService; + private readonly IWebHookService _webHookService; + + public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService) + { + _webhookFiringService = webhookFiringService; + _webHookService = webHookService; + EventName = Constants.WebhookEvents.ContentPublish; + } public override async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) { - // Implement your handling logic for the ContentPublish event - // You can access properties of the ContentSavedNotification here - await Task.Yield(); // Replace with your actual async logic + IEnumerable all = await _webHookService.GetAllAsync(); + IEnumerable webhooks = all.Where(x => x.Events.Contains(EventName)); + + foreach (Webhook webhook in webhooks) + { + foreach (IContent content in notification.PublishedEntities) + { + if (webhook.EntityKeys.Contains(content.ContentType.Key) is false) + { + continue; + } + + HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, content); + + // TODO: Implement logging depending on response here + if (response.IsSuccessStatusCode) + { + + } + } + } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index 34dc96e527d7..e32478efa8eb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -106,4 +106,17 @@ public async Task Can_Update() Assert.AreEqual(1, updatedWebhook.Events.Length); Assert.IsTrue(updatedWebhook.Events.Contains(Constants.WebhookEvents.ContentDelete)); } + + [Test] + public async Task Can_Get_By_EventName() + { + var webhook1 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentPublish })); + var webhook2 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentUnpublish })); + var webhook3 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentUnpublish })); + + var result = await WebhookService.GetByEventName(Constants.WebhookEvents.ContentUnpublish); + + Assert.IsNotEmpty(result); + Assert.AreEqual(2, result.Count()); + } } From be29bd27a087f102826b84e15ec3492932be34a7 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:56:06 +0200 Subject: [PATCH 051/102] Add 1 to many list on WebhookDto --- src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs index 381cd113375f..6fb219f5a053 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -24,5 +24,13 @@ internal class WebhookDto [Column(Name = "enabled")] public bool Enabled { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = nameof(Event2WebhookDto.WebhookId))] + public List Event2WebhookDtos { get; set; } = null!; + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = nameof(EntityKey2WebhookDto.WebhookId))] + public List EntityKey2WebhookDtos { get; set; } = null!; } From 4d5e01a03eadd0a26acc10ce9d87072a59f208de Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 27 Sep 2023 13:57:04 +0200 Subject: [PATCH 052/102] Implement new repository pattern --- .../Repositories/IWebhookRepository.cs | 37 ++++- src/Umbraco.Core/Services/IWebHookService.cs | 5 +- src/Umbraco.Core/Services/WebhookService.cs | 73 ++++---- .../Webhooks/ContentPublishWebhookEvent.cs | 4 +- .../Persistence/Dtos/WebhookDto.cs | 4 +- .../Persistence/Factories/WebhookFactory.cs | 8 +- .../Implement/WebhookRepository.cs | 157 ++++++++---------- .../Controllers/BackOfficeServerVariables.cs | 2 +- .../Controllers/WebHookController.cs | 6 +- .../Services/WebhookServiceTests.cs | 26 +-- 10 files changed, 152 insertions(+), 170 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs index 897fc0f2c295..f6fbe962b0ee 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs @@ -2,6 +2,41 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; -public interface IWebhookRepository : IReadWriteQueryRepository +public interface IWebhookRepository { + /// + /// Gets all of the webhooks in the current database. + /// + /// Number of entries to skip. + /// Number of entries to 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); + + /// + /// Gets a webhook by key + /// + /// The key of the webhook which will be retrieved. + /// The webhook with the given key. + Task GetAsync(Guid key); + + /// + /// Gets a webhook by key + /// + /// The webhook to be deleted. + /// A representing the asynchronous operation. + Task DeleteAsync(Webhook webhook); + + /// + /// Updates a given webhook + /// + /// The webhook to be updated. + /// The updated webhook. + Task UpdateAsync(Webhook webhook); } diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs index 49fe0ce68778..939f5f1de25b 100644 --- a/src/Umbraco.Core/Services/IWebHookService.cs +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -5,15 +5,14 @@ namespace Umbraco.Cms.Core.Services; public interface IWebHookService { Task CreateAsync(Webhook webhook); + Task UpdateAsync(Webhook updateModel); Task DeleteAsync(Guid key); Task GetAsync(Guid key); - Task> GetMultipleAsync(IEnumerable keys); - - Task> GetAllAsync(); + Task> GetAllAsync(int skip, int take); Task> GetByEventName(string eventName); } diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 1639312ba76f..ffb069e59ae3 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -1,37 +1,36 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services; -public class WebhookService : RepositoryService, IWebHookService +public class WebhookService : IWebHookService { + private readonly ICoreScopeProvider _provider; private readonly IWebhookRepository _webhookRepository; - public WebhookService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IWebhookRepository webhookRepository) : base(provider, loggerFactory, eventMessagesFactory) + public WebhookService(ICoreScopeProvider provider, IWebhookRepository webhookRepository) { + _provider = provider; _webhookRepository = webhookRepository; } - public Task CreateAsync(Webhook webhook) + public async Task CreateAsync(Webhook webhook) { - using ICoreScope scope = ScopeProvider.CreateCoreScope(); - _webhookRepository.Save(webhook); + using ICoreScope scope = _provider.CreateCoreScope(); + Webhook created = await _webhookRepository.CreateAsync(webhook); scope.Complete(); - return Task.FromResult(webhook); + return created; } - public Task UpdateAsync(Webhook updateModel) + public async Task UpdateAsync(Webhook updateModel) { - using ICoreScope scope = ScopeProvider.CreateCoreScope(); + using ICoreScope scope = _provider.CreateCoreScope(); // TODO: Validation if we need it - Webhook? currentWebhook = _webhookRepository.Get(updateModel.Key); + Webhook? currentWebhook = await _webhookRepository.GetAsync(updateModel.Key); if (currentWebhook is null) { @@ -43,55 +42,47 @@ public Task UpdateAsync(Webhook updateModel) currentWebhook.Events = updateModel.Events; currentWebhook.Url = updateModel.Url; - _webhookRepository.Save(currentWebhook); + await _webhookRepository.UpdateAsync(currentWebhook); scope.Complete(); - - return Task.FromResult(updateModel); } - public Task DeleteAsync(Guid key) + public async Task DeleteAsync(Guid key) { - using ICoreScope scope = ScopeProvider.CreateCoreScope(); - Webhook? webhook = _webhookRepository.Get(key); + using ICoreScope scope = _provider.CreateCoreScope(); + Webhook? webhook = await _webhookRepository.GetAsync(key); if (webhook is not null) { - _webhookRepository.Delete(webhook); + await _webhookRepository.DeleteAsync(webhook); } scope.Complete(); - return Task.CompletedTask; } - public Task GetAsync(Guid key) + public async Task GetAsync(Guid key) { - using ICoreScope scope = ScopeProvider.CreateCoreScope(); - Webhook? webhook = _webhookRepository.Get(key); + using ICoreScope scope = _provider.CreateCoreScope(); + Webhook? webhook = await _webhookRepository.GetAsync(key); scope.Complete(); - return Task.FromResult(webhook); + return webhook; } - public Task> GetMultipleAsync(IEnumerable keys) + public async Task> GetAllAsync(int skip, int take) { - using ICoreScope scope = ScopeProvider.CreateCoreScope(); - IEnumerable webhooks = _webhookRepository.GetMany(keys.ToArray()); + using ICoreScope scope = _provider.CreateCoreScope(); + PagedModel webhooks = await _webhookRepository.GetAllAsync(skip, take); scope.Complete(); - return Task.FromResult(webhooks); - } - public Task> GetAllAsync() - { - using ICoreScope scope = ScopeProvider.CreateCoreScope(); - IEnumerable webhooks = _webhookRepository.GetMany(); - scope.Complete(); - return Task.FromResult(webhooks); + return webhooks; } public Task> GetByEventName(string eventName) { - using ICoreScope scope = ScopeProvider.CreateCoreScope(); - IQuery query = Query().Where(x => x.Events.Contains(eventName)); - IEnumerable webhooks = _webhookRepository.Get(query); - scope.Complete(); - return Task.FromResult(webhooks); + // using ICoreScope scope = ScopeProvider.CreateCoreScope(); + // IQuery query = Query().Where(x => x.Events.Contains(eventName)); + // IEnumerable webhooks = _webhookRepository.Get(query); + // scope.Complete(); + // return Task.FromResult(webhooks); + return Task.FromResult(Enumerable.Empty()); + } } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 88cc74a4637c..f8b1533759b6 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -18,8 +18,8 @@ public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IW public override async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) { - IEnumerable all = await _webHookService.GetAllAsync(); - IEnumerable webhooks = all.Where(x => x.Events.Contains(EventName)); + PagedModel all = await _webHookService.GetAllAsync(0, int.MaxValue); + IEnumerable webhooks = all.Items.Where(x => x.Events.Contains(EventName)); foreach (Webhook webhook in webhooks) { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs index 6fb219f5a053..febdbaba1c77 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -27,10 +27,10 @@ internal class WebhookDto [ResultColumn] [Reference(ReferenceType.Many, ReferenceMemberName = nameof(Event2WebhookDto.WebhookId))] - public List Event2WebhookDtos { get; set; } = null!; + public List Event2WebhookDtos { get; set; } = new(); [ResultColumn] [Reference(ReferenceType.Many, ReferenceMemberName = nameof(EntityKey2WebhookDto.WebhookId))] - public List EntityKey2WebhookDtos { get; set; } = null!; + public List EntityKey2WebhookDtos { get; set; } = new(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index 642ee66c1bed..c4f9f20f8d94 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -47,17 +47,17 @@ public static WebhookDto BuildDto(Webhook webhook) return dto; } - public static IEnumerable BuildEntityKey2WebhookDto(Webhook webhook, int webhookId) => + public static IEnumerable BuildEntityKey2WebhookDto(Webhook webhook) => webhook.EntityKeys.Select(x => new EntityKey2WebhookDto { EntityKey = x, - WebhookId = webhookId, + WebhookId = webhook.Id, }); - public static IEnumerable BuildEvent2WebhookDto(Webhook webhook, int webhookId) => + public static IEnumerable BuildEvent2WebhookDto(Webhook webhook) => webhook.Events.Select(x => new Event2WebhookDto { Event = x, - WebhookId = webhookId, + WebhookId = webhook.Id, }); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index c449fc505ff4..e755c87c648f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -1,129 +1,117 @@ -using Microsoft.Extensions.Logging; -using NPoco; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Cache; +using NPoco; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; -public class WebhookRepository : EntityRepositoryBase, IWebhookRepository +public class WebhookRepository : IWebhookRepository { - public WebhookRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) : base(scopeAccessor, appCaches, logger) - { - } + private readonly IScopeAccessor _scopeAccessor; + + public WebhookRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; - protected override Webhook? PerformGet(Guid key) + public async Task> GetAllAsync(int skip, int take) { - Sql sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { key }); + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From(); - WebhookDto? dto = Database.FirstOrDefault(sql); + List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; - if (dto is not null) - { - return DtoToEntity(dto); - } + IEnumerable? webhooks = webhookDtos?.Skip(skip).Take(take).Select(DtoToEntity).WhereNotNull(); - return null; + return new PagedModel + { + Items = webhooks ?? Enumerable.Empty(), + Total = webhookDtos?.Count ?? 0, + }; } - protected override IEnumerable PerformGetAll(params Guid[]? ids) + public async Task CreateAsync(Webhook webhook) { - Sql sql = GetBaseQuery(false); + webhook.AddingEntity(); - List dtos = Database.Fetch(sql); + WebhookDto webhookDto = WebhookFactory.BuildDto(webhook); - return dtos.Select(DtoToEntity); - } + var result = await _scopeAccessor.AmbientScope?.Database.InsertAsync(webhookDto)!; - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); + var id = Convert.ToInt32(result); + webhook.Id = id; + + IEnumerable entityKeys = WebhookFactory.BuildEvent2WebhookDto(webhook); + await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(entityKeys)!; + await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(WebhookFactory.BuildEntityKey2WebhookDto(webhook))!; - List? dtos = Database.Fetch(sql); + webhook.ResetDirtyProperties(); - return dtos.Select(DtoToEntity); + return webhook; } - protected override void PersistNewItem(Webhook entity) + public async Task GetAsync(Guid key) { - entity.AddingEntity(); + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From() + .Where(x => x.Key == key); - WebhookDto webhookDto = WebhookFactory.BuildDto(entity); + WebhookDto? webhookDto = await _scopeAccessor.AmbientScope?.Database.FirstOrDefaultAsync(sql)!; - var id = Convert.ToInt32(Database.Insert(webhookDto)); - entity.Id = id; - InsertManyToOneReferences(entity); - - entity.ResetDirtyProperties(); + return DtoToEntity(webhookDto); } - protected override void PersistUpdatedItem(Webhook entity) + public async Task DeleteAsync(Webhook webhook) { - entity.UpdatingEntity(); - - WebhookDto dto = WebhookFactory.BuildDto(entity); - Database.Update(dto); + Sql sql = _scopeAccessor.AmbientScope!.Database.SqlContext.Sql() + .Delete() + .Where(x => x.Key == webhook.Key); - // Delete and re-insert the many to one references (event & entity keys) - DeleteUpdateManyToOneReferences(dto.Id); - InsertManyToOneReferences(entity); + await _scopeAccessor.AmbientScope?.Database.ExecuteAsync(sql)!; - entity.ResetDirtyProperties(); + webhook.DeleteDate = DateTime.Now; } - protected override Sql GetBaseQuery(bool isCount) + public async Task UpdateAsync(Webhook webhook) { - Sql sql = Sql(); + webhook.UpdatingEntity(); - sql = isCount - ? sql.SelectCount() - : sql.Select(); + WebhookDto dto = WebhookFactory.BuildDto(webhook); + await _scopeAccessor.AmbientScope?.Database.UpdateAsync(dto)!; - sql - .From(); + // Delete and re-insert the many to one references (event & entity keys) + DeleteManyToOneReferences(dto.Id); + InsertManyToOneReferences(webhook); - return sql; + webhook.ResetDirtyProperties(); } - protected override string GetBaseWhereClause() => "key = @key"; - - protected override IEnumerable GetDeleteClauses() + private void DeleteManyToOneReferences(int webhookId) { - var list = new List - { - $"DELETE FROM {Constants.DatabaseSchema.Tables.Webhook} WHERE key = @key", - }; - return list; + _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); + _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); } - protected override Guid GetEntityId(Webhook entity) - => entity.Key; - - protected override void PersistDeletedItem(Webhook entity) + private void InsertManyToOneReferences(Webhook webhook) { - IEnumerable deletes = GetDeleteClauses(); - foreach (var delete in deletes) - { - Database.Execute(delete, new { key = GetEntityId(entity) }); - } + IEnumerable buildEntityKey2WebhookDtos = WebhookFactory.BuildEntityKey2WebhookDto(webhook); + IEnumerable buildEvent2WebhookDtos = WebhookFactory.BuildEvent2WebhookDto(webhook); - entity.DeleteDate = DateTime.Now; + _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(buildEntityKey2WebhookDtos); + _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(buildEvent2WebhookDtos); } - private Webhook DtoToEntity(WebhookDto dto) + private Webhook? DtoToEntity(WebhookDto? dto) { - List webhookEntityKeyDtos = Database.Fetch("WHERE webhookId = @webhookId", new { webhookId = dto.Id }); - List event2WebhookDtos = Database.Fetch("WHERE webhookId = @webhookId", new { webhookId = dto.Id }); + if (dto is null) + { + return null; + } + + List? webhookEntityKeyDtos = _scopeAccessor.AmbientScope?.Database.Fetch("WHERE webhookId = @webhookId", new { webhookId = dto.Id }); + List? event2WebhookDtos = _scopeAccessor.AmbientScope?.Database.Fetch("WHERE webhookId = @webhookId", new { webhookId = dto.Id }); Webhook entity = WebhookFactory.BuildEntity(dto, webhookEntityKeyDtos, event2WebhookDtos); // reset dirty initial properties (U4-1946) @@ -131,19 +119,4 @@ private Webhook DtoToEntity(WebhookDto dto) return entity; } - - private void DeleteUpdateManyToOneReferences(int webhookId) - { - Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); - Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); - } - - private void InsertManyToOneReferences(Webhook webhook) - { - IEnumerable buildEntityKey2WebhookDtos = WebhookFactory.BuildEntityKey2WebhookDto(webhook, webhook.Id); - IEnumerable buildEvent2WebhookDtos = WebhookFactory.BuildEvent2WebhookDto(webhook, webhook.Id); - - Database.InsertBulk(buildEntityKey2WebhookDtos); - Database.InsertBulk(buildEvent2WebhookDtos); - } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 13d41177baef..701d50333941 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -587,7 +587,7 @@ internal async Task> GetServerVariablesAsync() }, { "webhooksApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAll()) + controller => controller.GetAll(0, 0)) }, } }, diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs index 155833e55da5..9a35f5326d91 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -22,11 +22,11 @@ public WebHookController(IWebHookService webHookService, IUmbracoMapper umbracoM } [HttpGet] - public async Task GetAll() + public async Task GetAll(int skip = 0, int take = int.MaxValue) { - IEnumerable webhooks = await _webHookService.GetAllAsync(); + PagedModel webhooks = await _webHookService.GetAllAsync(skip, take); - List webhookViewModels = _umbracoMapper.MapEnumerable(webhooks); + List webhookViewModels = _umbracoMapper.MapEnumerable(webhooks.Items); return Ok(webhookViewModels); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index e32478efa8eb..8113c39b5ce1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -34,36 +34,20 @@ public async Task Can_Create_And_Get(string url, string webhookEvent, Guid key) }); } - [Test] - public async Task Can_Get_Multiple() - { - var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentPublish })); - var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentDelete })); - var keys = new List { createdWebhookOne.Key, createdWebhookTwo.Key }; - var webhooks = await WebhookService.GetMultipleAsync(keys); - - Assert.Multiple(() => - { - Assert.IsNotEmpty(webhooks); - Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookOne.Key)); - Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookTwo.Key)); - }); - } - [Test] public async Task Can_Get_All() { var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentPublish })); var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentDelete })); var createdWebhookThree = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentUnpublish })); - var webhooks = await WebhookService.GetAllAsync(); + var webhooks = await WebhookService.GetAllAsync(0, int.MaxValue); Assert.Multiple(() => { - Assert.IsNotEmpty(webhooks); - Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookOne.Key)); - Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookTwo.Key)); - Assert.IsNotNull(webhooks.FirstOrDefault(x => x.Key == createdWebhookThree.Key)); + Assert.IsNotEmpty(webhooks.Items); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookOne.Key)); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookTwo.Key)); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookThree.Key)); }); } From 8fc0079ff47cb2ff03754495f0173c22b41591e5 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 27 Sep 2023 14:50:09 +0200 Subject: [PATCH 053/102] Implement GetByEventName --- .../Repositories/IWebhookRepository.cs | 7 +++++++ src/Umbraco.Core/Services/IWebHookService.cs | 2 +- src/Umbraco.Core/Services/WebhookService.cs | 12 +++++------ .../Implement/WebhookRepository.cs | 20 +++++++++++++++++++ .../Services/WebhookServiceTests.cs | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs index f6fbe962b0ee..d045cd172f6f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs @@ -26,6 +26,13 @@ public interface IWebhookRepository /// The webhook with the given key. Task GetAsync(Guid key); + /// + /// Gets a webhook by key + /// + /// The key of the webhook which will be retrieved. + /// The webhook with the given key. + Task> GetByEventNameAsync(string eventName); + /// /// Gets a webhook by key /// diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs index 939f5f1de25b..afbab0e13723 100644 --- a/src/Umbraco.Core/Services/IWebHookService.cs +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -14,5 +14,5 @@ public interface IWebHookService Task> GetAllAsync(int skip, int take); - Task> GetByEventName(string eventName); + Task> GetByEventNameAsync(string eventName); } diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index ffb069e59ae3..0ffed52f2af3 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -75,14 +75,12 @@ public async Task> GetAllAsync(int skip, int take) return webhooks; } - public Task> GetByEventName(string eventName) + public async Task> GetByEventNameAsync(string eventName) { - // using ICoreScope scope = ScopeProvider.CreateCoreScope(); - // IQuery query = Query().Where(x => x.Events.Contains(eventName)); - // IEnumerable webhooks = _webhookRepository.Get(query); - // scope.Complete(); - // return Task.FromResult(webhooks); - return Task.FromResult(Enumerable.Empty()); + using ICoreScope scope = _provider.CreateCoreScope(); + PagedModel webhooks = await _webhookRepository.GetByEventNameAsync(eventName); + scope.Complete(); + return webhooks.Items; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index e755c87c648f..964fa19e34f4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -63,6 +63,26 @@ public async Task CreateAsync(Webhook webhook) return DtoToEntity(webhookDto); } + public async Task> GetByEventNameAsync(string eventName) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From() + .InnerJoin() + .On(left => left.Id, right => right.WebhookId) + .Where(x => x.Event == eventName); + + var webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; + + IEnumerable? webhooks = webhookDtos?.Select(DtoToEntity).WhereNotNull(); + + return new PagedModel + { + Items = webhooks ?? Enumerable.Empty(), + Total = webhookDtos?.Count ?? 0, + }; + } + public async Task DeleteAsync(Webhook webhook) { Sql sql = _scopeAccessor.AmbientScope!.Database.SqlContext.Sql() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index 8113c39b5ce1..4be47d221496 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -98,7 +98,7 @@ public async Task Can_Get_By_EventName() var webhook2 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentUnpublish })); var webhook3 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentUnpublish })); - var result = await WebhookService.GetByEventName(Constants.WebhookEvents.ContentUnpublish); + var result = await WebhookService.GetByEventNameAsync(Constants.WebhookEvents.ContentUnpublish); Assert.IsNotEmpty(result); Assert.AreEqual(2, result.Count()); From ba501ef055cb329af32f63a0af245433ea797b7c Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 28 Sep 2023 09:13:09 +0200 Subject: [PATCH 054/102] Fix up repository to use all async --- .../Implement/WebhookRepository.cs | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index 964fa19e34f4..de0f4d2d0cee 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -22,12 +22,10 @@ public async Task> GetAllAsync(int skip, int take) List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; - IEnumerable? webhooks = webhookDtos?.Skip(skip).Take(take).Select(DtoToEntity).WhereNotNull(); - return new PagedModel { - Items = webhooks ?? Enumerable.Empty(), - Total = webhookDtos?.Count ?? 0, + Items = await DtosToEntities(webhookDtos), + Total = webhookDtos.Count, }; } @@ -60,7 +58,7 @@ public async Task CreateAsync(Webhook webhook) WebhookDto? webhookDto = await _scopeAccessor.AmbientScope?.Database.FirstOrDefaultAsync(sql)!; - return DtoToEntity(webhookDto); + return webhookDto is null ? null : await DtoToEntity(webhookDto); } public async Task> GetByEventNameAsync(string eventName) @@ -72,14 +70,12 @@ public async Task> GetByEventNameAsync(string eventName) .On(left => left.Id, right => right.WebhookId) .Where(x => x.Event == eventName); - var webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; - - IEnumerable? webhooks = webhookDtos?.Select(DtoToEntity).WhereNotNull(); + List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; return new PagedModel { - Items = webhooks ?? Enumerable.Empty(), - Total = webhookDtos?.Count ?? 0, + Items = await DtosToEntities(webhookDtos), + Total = webhookDtos.Count, }; } @@ -123,15 +119,22 @@ private void InsertManyToOneReferences(Webhook webhook) _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(buildEvent2WebhookDtos); } - private Webhook? DtoToEntity(WebhookDto? dto) + private async Task> DtosToEntities(IEnumerable dtos) { - if (dto is null) + List result = new(); + + foreach (WebhookDto webhook in dtos) { - return null; + result.Add(await DtoToEntity(webhook)); } - List? webhookEntityKeyDtos = _scopeAccessor.AmbientScope?.Database.Fetch("WHERE webhookId = @webhookId", new { webhookId = dto.Id }); - List? event2WebhookDtos = _scopeAccessor.AmbientScope?.Database.Fetch("WHERE webhookId = @webhookId", new { webhookId = dto.Id }); + return result; + } + + 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 })!; Webhook entity = WebhookFactory.BuildEntity(dto, webhookEntityKeyDtos, event2WebhookDtos); // reset dirty initial properties (U4-1946) From 818be73dbe53f245528fdda4c186a2812d17a2f1 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 28 Sep 2023 10:55:01 +0200 Subject: [PATCH 055/102] Refactor events to fire --- src/Umbraco.Core/Constants-WebhookEvents.cs | 2 +- .../Webhooks/ContentDeleteWebhookEvent.cs | 37 +++++++++++++++--- .../Webhooks/ContentPublishWebhookEvent.cs | 3 +- .../Webhooks/ContentUnpublishWebhookEvent.cs | 34 +++++++++++++++-- .../Webhooks/MediaDeleteWebhookEvent.cs | 38 +++++++++++++++++-- .../Webhooks/MediaSaveWebhookEvent.cs | 38 +++++++++++++++++-- 6 files changed, 135 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Core/Constants-WebhookEvents.cs b/src/Umbraco.Core/Constants-WebhookEvents.cs index dbe48e7babe3..24fe8902212b 100644 --- a/src/Umbraco.Core/Constants-WebhookEvents.cs +++ b/src/Umbraco.Core/Constants-WebhookEvents.cs @@ -27,6 +27,6 @@ public static class WebhookEvents /// /// Webhook event name for media save. /// - public const string MediaSave = "MediaDelete"; + public const string MediaSave = "MediaSave"; } } diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index 1990025e812e..d94730558b86 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -1,15 +1,42 @@ -using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentDeleteWebhookEvent : WebhookEventBase { - public ContentDeleteWebhookEvent() => EventName = Constants.WebhookEvents.ContentDelete; + private readonly IWebHookService _webHookService; + private readonly IWebhookFiringService _webhookFiringService; + + public ContentDeleteWebhookEvent(IWebHookService webHookService, IWebhookFiringService webhookFiringService) + { + _webHookService = webHookService; + _webhookFiringService = webhookFiringService; + EventName = Constants.WebhookEvents.ContentDelete; + } public override async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) { - // Implement your handling logic for the ContentPublish event - // You can access properties of the ContentSavedNotification here - await Task.Yield(); // Replace with your actual async logic + IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); + + foreach (Webhook webhook in webhooks) + { + foreach (IContent content in notification.DeletedEntities) + { + if (webhook.EntityKeys.Contains(content.ContentType.Key) is false) + { + continue; + } + + HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, content); + + // TODO: Implement logging depending on response here + if (response.IsSuccessStatusCode) + { + + } + } + } } } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index f8b1533759b6..6a713eaa9b8b 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -18,8 +18,7 @@ public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IW public override async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) { - PagedModel all = await _webHookService.GetAllAsync(0, int.MaxValue); - IEnumerable webhooks = all.Items.Where(x => x.Events.Contains(EventName)); + IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); foreach (Webhook webhook in webhooks) { diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index c553e8a9ca7f..f7e52b0ff837 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -1,13 +1,41 @@ -using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentUnpublishWebhookEvent : WebhookEventBase { - public ContentUnpublishWebhookEvent() => EventName = Constants.WebhookEvents.ContentUnpublish; + private readonly IWebHookService _webHookService; + private readonly IWebhookFiringService _webhookFiringService; + public ContentUnpublishWebhookEvent(IWebHookService webHookService, IWebhookFiringService webhookFiringService) + { + _webHookService = webHookService; + _webhookFiringService = webhookFiringService; + EventName = Constants.WebhookEvents.ContentUnpublish; + } public override async Task HandleAsync(ContentUnpublishedNotification notification, CancellationToken cancellationToken) { - await Task.Yield(); // Replace with your actual async logic + IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); + + foreach (Webhook webhook in webhooks) + { + foreach (IContent content in notification.UnpublishedEntities) + { + if (webhook.EntityKeys.Contains(content.ContentType.Key) is false) + { + continue; + } + + HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, content); + + // TODO: Implement logging depending on response here + if (response.IsSuccessStatusCode) + { + + } + } + } } } diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index a49cfaab78ae..b6f99adc62ed 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -1,10 +1,42 @@ -using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class MediaDeleteWebhookEvent : WebhookEventBase { - public MediaDeleteWebhookEvent() => EventName = Constants.WebhookEvents.MediaDelete; + private readonly IWebhookFiringService _webhookFiringService; + private readonly IWebHookService _webHookService; - public override Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); + public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService) + { + _webhookFiringService = webhookFiringService; + _webHookService = webHookService; + EventName = Constants.WebhookEvents.MediaDelete; + } + + public override async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) + { + IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); + + foreach (Webhook webhook in webhooks) + { + foreach (IMedia media in notification.DeletedEntities) + { + if (webhook.EntityKeys.Contains(media.ContentType.Key) is false) + { + continue; + } + + HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, media); + + // TODO: Implement logging depending on response here + if (response.IsSuccessStatusCode) + { + + } + } + } + } } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index aa4fe37caeb7..a32c6e1d6482 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -1,10 +1,42 @@ -using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class MediaSaveWebhookEvent : WebhookEventBase { - public MediaSaveWebhookEvent() => EventName = Constants.WebhookEvents.MediaSave; + private readonly IWebhookFiringService _webhookFiringService; + private readonly IWebHookService _webHookService; - public override Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); + public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService) + { + _webhookFiringService = webhookFiringService; + _webHookService = webHookService; + EventName = Constants.WebhookEvents.MediaSave; + } + + public override async Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken) + { + IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); + + foreach (Webhook webhook in webhooks) + { + foreach (IMedia media in notification.SavedEntities) + { + if (webhook.EntityKeys.Contains(media.ContentType.Key) is false) + { + continue; + } + + HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, media); + + // TODO: Implement logging depending on response here + if (response.IsSuccessStatusCode) + { + + } + } + } + } } From 2e9184d18b4fa00fef22b3fde4e2a130e9f53d68 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 28 Sep 2023 11:25:44 +0200 Subject: [PATCH 056/102] Refactor WebhookEvents to be more DRY --- .../Webhooks/ContentDeleteWebhookEvent.cs | 35 ++------------- .../Webhooks/ContentPublishWebhookEvent.cs | 35 ++------------- .../Webhooks/ContentUnpublishWebhookEvent.cs | 34 ++------------ .../Webhooks/MediaDeleteWebhookEvent.cs | 35 ++------------- .../Webhooks/MediaSaveWebhookEvent.cs | 35 ++------------- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 45 +++++++++++++++++-- 6 files changed, 61 insertions(+), 158 deletions(-) diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index d94730558b86..68f196a26734 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -4,39 +4,12 @@ namespace Umbraco.Cms.Core.Webhooks; -public class ContentDeleteWebhookEvent : WebhookEventBase +public class ContentDeleteWebhookEvent : WebhookEventBase { - private readonly IWebHookService _webHookService; - private readonly IWebhookFiringService _webhookFiringService; - - public ContentDeleteWebhookEvent(IWebHookService webHookService, IWebhookFiringService webhookFiringService) + public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) + : base(webhookFiringService, webHookService, eventName) { - _webHookService = webHookService; - _webhookFiringService = webhookFiringService; - EventName = Constants.WebhookEvents.ContentDelete; } - public override async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) - { - IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); - - foreach (Webhook webhook in webhooks) - { - foreach (IContent content in notification.DeletedEntities) - { - if (webhook.EntityKeys.Contains(content.ContentType.Key) is false) - { - continue; - } - - HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, content); - - // TODO: Implement logging depending on response here - if (response.IsSuccessStatusCode) - { - - } - } - } - } + protected override IEnumerable GetEntitiesFromNotification(ContentDeletedNotification notification) => notification.DeletedEntities; } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 6a713eaa9b8b..796f70e2d6ec 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -4,39 +4,12 @@ namespace Umbraco.Cms.Core.Webhooks; -public class ContentPublishWebhookEvent : WebhookEventBase +public class ContentPublishWebhookEvent : WebhookEventBase { - private readonly IWebhookFiringService _webhookFiringService; - private readonly IWebHookService _webHookService; - - public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService) + public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) + : base(webhookFiringService, webHookService, eventName) { - _webhookFiringService = webhookFiringService; - _webHookService = webHookService; - EventName = Constants.WebhookEvents.ContentPublish; } - public override async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) - { - IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); - - foreach (Webhook webhook in webhooks) - { - foreach (IContent content in notification.PublishedEntities) - { - if (webhook.EntityKeys.Contains(content.ContentType.Key) is false) - { - continue; - } - - HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, content); - - // TODO: Implement logging depending on response here - if (response.IsSuccessStatusCode) - { - - } - } - } - } + protected override IEnumerable GetEntitiesFromNotification(ContentPublishedNotification notification) => notification.PublishedEntities; } diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index f7e52b0ff837..1659847771db 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -4,38 +4,12 @@ namespace Umbraco.Cms.Core.Webhooks; -public class ContentUnpublishWebhookEvent : WebhookEventBase +public class ContentUnpublishWebhookEvent : WebhookEventBase { - private readonly IWebHookService _webHookService; - private readonly IWebhookFiringService _webhookFiringService; - public ContentUnpublishWebhookEvent(IWebHookService webHookService, IWebhookFiringService webhookFiringService) + public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) + : base(webhookFiringService, webHookService, eventName) { - _webHookService = webHookService; - _webhookFiringService = webhookFiringService; - EventName = Constants.WebhookEvents.ContentUnpublish; } - public override async Task HandleAsync(ContentUnpublishedNotification notification, CancellationToken cancellationToken) - { - IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); - - foreach (Webhook webhook in webhooks) - { - foreach (IContent content in notification.UnpublishedEntities) - { - if (webhook.EntityKeys.Contains(content.ContentType.Key) is false) - { - continue; - } - - HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, content); - - // TODO: Implement logging depending on response here - if (response.IsSuccessStatusCode) - { - - } - } - } - } + protected override IEnumerable GetEntitiesFromNotification(ContentUnpublishedNotification notification) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index b6f99adc62ed..77eec0be335e 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -4,39 +4,12 @@ namespace Umbraco.Cms.Core.Webhooks; -public class MediaDeleteWebhookEvent : WebhookEventBase +public class MediaDeleteWebhookEvent : WebhookEventBase { - private readonly IWebhookFiringService _webhookFiringService; - private readonly IWebHookService _webHookService; - - public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService) + public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) + : base(webhookFiringService, webHookService, eventName) { - _webhookFiringService = webhookFiringService; - _webHookService = webHookService; - EventName = Constants.WebhookEvents.MediaDelete; } - public override async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) - { - IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); - - foreach (Webhook webhook in webhooks) - { - foreach (IMedia media in notification.DeletedEntities) - { - if (webhook.EntityKeys.Contains(media.ContentType.Key) is false) - { - continue; - } - - HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, media); - - // TODO: Implement logging depending on response here - if (response.IsSuccessStatusCode) - { - - } - } - } - } + protected override IEnumerable GetEntitiesFromNotification(MediaDeletedNotification notification) => notification.DeletedEntities; } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index a32c6e1d6482..e9c72d0dfe09 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -4,39 +4,12 @@ namespace Umbraco.Cms.Core.Webhooks; -public class MediaSaveWebhookEvent : WebhookEventBase +public class MediaSaveWebhookEvent : WebhookEventBase { - private readonly IWebhookFiringService _webhookFiringService; - private readonly IWebHookService _webHookService; - - public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService) + public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) + : base(webhookFiringService, webHookService, eventName) { - _webhookFiringService = webhookFiringService; - _webHookService = webHookService; - EventName = Constants.WebhookEvents.MediaSave; } - public override async Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken) - { - IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); - - foreach (Webhook webhook in webhooks) - { - foreach (IMedia media in notification.SavedEntities) - { - if (webhook.EntityKeys.Contains(media.ContentType.Key) is false) - { - continue; - } - - HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, media); - - // TODO: Implement logging depending on response here - if (response.IsSuccessStatusCode) - { - - } - } - } - } + protected override IEnumerable GetEntitiesFromNotification(MediaSavedNotification notification) => notification.SavedEntities; } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 8daa232bee7b..c8faea1beb3a 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -1,14 +1,51 @@ using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; -public abstract class WebhookEventBase : IWebhookEvent, INotificationAsyncHandler - where T : INotification +public abstract class WebhookEventBase : IWebhookEvent, INotificationAsyncHandler + where TNotification : INotification + where TEntity : IContentBase { - public string EventName { get; set; } = string.Empty; + private readonly IWebhookFiringService _webhookFiringService; + private readonly IWebHookService _webHookService; - public abstract Task HandleAsync(T notification, CancellationToken cancellationToken); + protected WebhookEventBase(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) + { + _webhookFiringService = webhookFiringService; + _webHookService = webHookService; + EventName = eventName; + } + + public string EventName { get; set; } + + public async Task HandleAsync(TNotification notification, CancellationToken cancellationToken) + { + IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); + + foreach (Webhook webhook in webhooks) + { + foreach (TEntity entity in GetEntitiesFromNotification(notification)) + { + if (!webhook.EntityKeys.Contains(entity.ContentType.Key)) + { + continue; + } + + HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, entity); + + // TODO: Implement logging depending on response here + if (response.IsSuccessStatusCode) + { + // Handle success + } + } + } + } + + protected abstract IEnumerable GetEntitiesFromNotification(TNotification notification); } From 1c77eb26b3126db8d9791f74a4e6109740652a6e Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 28 Sep 2023 13:54:46 +0200 Subject: [PATCH 057/102] Add custom header --- src/Umbraco.Core/Webhooks/IWebhookFiringService.cs | 2 +- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 2 +- .../Services/Implement/WebhookFiringService.cs | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs b/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs index 5238c4430a8f..a161bb2b8ea9 100644 --- a/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs +++ b/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs @@ -2,5 +2,5 @@ public interface IWebhookFiringService { - Task Fire( string url, object? requestObject); + Task Fire( string url, string eventName, object? requestObject); } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index c8faea1beb3a..67f6b3f6835d 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -35,7 +35,7 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc continue; } - HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, entity); + HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, EventName, entity); // TODO: Implement logging depending on response here if (response.IsSuccessStatusCode) diff --git a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs index 74a902ca23d1..bdb1b76d198f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Webhooks; namespace Umbraco.Cms.Infrastructure.Services.Implement; @@ -15,7 +16,7 @@ public WebhookFiringService(IJsonSerializer jsonSerializer, IRetryService retryS _retryService = retryService; } - public async Task Fire(string url, object? requestObject) => await _retryService.RetryAsync( + public async Task Fire(string url, string eventName, object? requestObject) => await _retryService.RetryAsync( async () => { using var httpClient = new HttpClient(); @@ -23,6 +24,7 @@ public async Task Fire(string url, object? requestObject) = var myContent = _jsonSerializer.Serialize(requestObject); var buffer = System.Text.Encoding.UTF8.GetBytes(myContent); var byteContent = new ByteArrayContent(buffer); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Umb-Webhook-Event", eventName); return await httpClient.PostAsync(url, byteContent); }, From 4859dd3a13835b3df1c3a4b2ac305e3fe039bd2c Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 28 Sep 2023 14:17:07 +0200 Subject: [PATCH 058/102] Start implementing log repository --- .../Persistence/Constants-DatabaseSchema.cs | 1 + .../Repositories/IWebhookLogRepository.cs | 11 +++++ .../Webhooks/ContentDeleteWebhookEvent.cs | 5 ++- .../Webhooks/ContentPublishWebhookEvent.cs | 5 ++- .../Webhooks/ContentUnpublishWebhookEvent.cs | 5 ++- .../Webhooks/MediaDeleteWebhookEvent.cs | 5 ++- .../Webhooks/MediaSaveWebhookEvent.cs | 5 ++- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 18 +++++--- src/Umbraco.Core/Webhooks/WebhookLog.cs | 18 ++++++++ .../UmbracoBuilder.Repositories.cs | 1 + .../Install/DatabaseSchemaCreator.cs | 1 + .../Persistence/Dtos/WebhookLogDto.cs | 43 +++++++++++++++++++ .../Persistence/Factories/WebhookFactory.cs | 1 - .../Factories/WebhookLogFactory.cs | 20 +++++++++ .../Implement/WebhookLogRepository.cs | 25 +++++++++++ 15 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs create mode 100644 src/Umbraco.Core/Webhooks/WebhookLog.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 1319e3893466..679561940524 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -89,6 +89,7 @@ public static class Tables public const string Webhook = TableNamePrefix + "Webhook"; public const string EntityKey2Webhook = TableNamePrefix + "EntityKey2Webhook"; public const string Event2Webhook = TableNamePrefix + "Event2Webhook"; + public const string WebhookLog = TableNamePrefix + "WebhookLog"; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs new file mode 100644 index 000000000000..a4652d595545 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IWebhookLogRepository +{ + Task CreateAsync(WebhookLog log); + + Task> GetPagedAsync(int skip, int take); +} diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index 68f196a26734..2f508fc2a17b 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -1,13 +1,14 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentDeleteWebhookEvent : WebhookEventBase { - public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) - : base(webhookFiringService, webHookService, eventName) + public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) + : base(webhookFiringService, webHookService, webhookLogRepository, eventName) { } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 796f70e2d6ec..1c82edcd27b5 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -1,13 +1,14 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentPublishWebhookEvent : WebhookEventBase { - public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) - : base(webhookFiringService, webHookService, eventName) + public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) + : base(webhookFiringService, webHookService, webhookLogRepository, eventName) { } diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index 1659847771db..f6c35425238f 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -1,13 +1,14 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentUnpublishWebhookEvent : WebhookEventBase { - public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) - : base(webhookFiringService, webHookService, eventName) + public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) + : base(webhookFiringService, webHookService, webhookLogRepository, eventName) { } diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index 77eec0be335e..f43e2612f536 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -1,13 +1,14 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class MediaDeleteWebhookEvent : WebhookEventBase { - public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) - : base(webhookFiringService, webHookService, eventName) + public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) + : base(webhookFiringService, webHookService, webhookLogRepository, eventName) { } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index e9c72d0dfe09..c1e66ca354ad 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -1,13 +1,14 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class MediaSaveWebhookEvent : WebhookEventBase { - public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) - : base(webhookFiringService, webHookService, eventName) + public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) + : base(webhookFiringService, webHookService, webhookLogRepository, eventName) { } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 67f6b3f6835d..0d9c0c9c385a 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -1,6 +1,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; @@ -12,11 +13,13 @@ public abstract class WebhookEventBase : IWebhookEvent, private readonly IWebhookFiringService _webhookFiringService; private readonly IWebHookService _webHookService; + private readonly IWebhookLogRepository _webhookLogRepository; - protected WebhookEventBase(IWebhookFiringService webhookFiringService, IWebHookService webHookService, string eventName) + protected WebhookEventBase(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) { _webhookFiringService = webhookFiringService; _webHookService = webHookService; + _webhookLogRepository = webhookLogRepository; EventName = eventName; } @@ -37,11 +40,16 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, EventName, entity); - // TODO: Implement logging depending on response here - if (response.IsSuccessStatusCode) + var log = new WebhookLog { - // Handle success - } + Date = DateTime.UtcNow, + EventName = EventName, + RequestBody = await response.RequestMessage!.Content!.ReadAsStringAsync(cancellationToken), + ResponseBody = await response.Content.ReadAsStringAsync(cancellationToken), + StatusCode = response.StatusCode.ToString(), + RetryCount = 0, + }; + await _webhookLogRepository.CreateAsync(log); } } } diff --git a/src/Umbraco.Core/Webhooks/WebhookLog.cs b/src/Umbraco.Core/Webhooks/WebhookLog.cs new file mode 100644 index 000000000000..29495189b4b2 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/WebhookLog.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Webhooks; + +public class WebhookLog +{ + public int Id { get; set; } + public Guid Key { get; set; } + public string StatusCode { get; set; } = string.Empty; + + public DateTime Date { get; set; } + + public string EventName { get; set; } = string.Empty; + + public int RetryCount { get; set; } + + public string RequestBody { get; set; } = string.Empty; + + public string ResponseBody { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 1d212f5d33e3..df2ac91839e8 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -69,6 +69,7 @@ internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 6bf35343aade..2351e026c0fc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -86,6 +86,7 @@ public class DatabaseSchemaCreator typeof(WebhookDto), typeof(EntityKey2WebhookDto), typeof(Event2WebhookDto), + typeof(WebhookLogDto), }; private readonly IUmbracoDatabase _database; diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs new file mode 100644 index 000000000000..037703675642 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs @@ -0,0 +1,43 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Webhook)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class WebhookLogDto +{ + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = true)] + public int Id { get; set; } + + [Column(Name = "key")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid Key { get; set; } + + [Column(Name = "statusCode")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string StatusCode { get; set; } = string.Empty; + + [Column(Name = "date")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime Date { get; set; } + + [Column(Name = "eventName")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string EventName { get; set; } = string.Empty; + + [Column(Name = "retryCount")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int RetryCount { get; set; } + + [Column(Name = "requestBody")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string RequestBody { get; set; } = string.Empty; + + [Column(Name = "responseBody")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string ResponseBody { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index c4f9f20f8d94..a619075b8fd7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -1,5 +1,4 @@ using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Factories; diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs new file mode 100644 index 000000000000..5e2655651073 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class WebhookLogFactory +{ + public static WebhookLogDto CreateDto(WebhookLog log) => + new() + { + Date = log.Date, + EventName = log.EventName, + RequestBody = log.RequestBody, + ResponseBody = log.ResponseBody, + RetryCount = log.RetryCount, + StatusCode = log.StatusCode, + Key = log.Key, + Id = log.Id, + }; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs new file mode 100644 index 000000000000..2f2474042f53 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs @@ -0,0 +1,25 @@ +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; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class WebhookLogRepository : IWebhookLogRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + 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 id = Convert.ToInt32(result); + log.Id = id; + } + + public Task> GetPagedAsync(int skip, int take) => throw new NotImplementedException(); +} From 4bd4aa11e27105a92439a5a416cb11eeb2099fa2 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:55:29 +0200 Subject: [PATCH 059/102] Implement GetPaged --- src/Umbraco.Core/PaginationHelper.cs | 15 +++++++++++++ .../Webhooks/ContentDeleteWebhookEvent.cs | 5 +++-- .../Webhooks/ContentPublishWebhookEvent.cs | 5 +++-- .../Webhooks/ContentUnpublishWebhookEvent.cs | 5 +++-- .../Webhooks/MediaDeleteWebhookEvent.cs | 5 +++-- .../Webhooks/MediaSaveWebhookEvent.cs | 5 +++-- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 8 ++++++- .../Persistence/Dtos/WebhookLogDto.cs | 2 +- .../Factories/WebhookLogFactory.cs | 13 +++++++++++ .../Implement/WebhookLogRepository.cs | 22 +++++++++++++++++-- .../Implement/WebhookRepository.cs | 2 +- 11 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 src/Umbraco.Core/PaginationHelper.cs diff --git a/src/Umbraco.Core/PaginationHelper.cs b/src/Umbraco.Core/PaginationHelper.cs new file mode 100644 index 000000000000..eb9049c1da98 --- /dev/null +++ b/src/Umbraco.Core/PaginationHelper.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core; + +public static class PaginationHelper +{ + public static void ConvertSkipTakeToPaging(int skip, int take, out long pageNumber, out int pageSize) + { + if (skip % take != 0) + { + throw new ArgumentException("Invalid skip/take, Skip must be a multiple of take - i.e. skip = 10, take = 5"); + } + + pageSize = take; + pageNumber = skip / take; + } +} diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index 2f508fc2a17b..90e198feb5cb 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -1,14 +1,15 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentDeleteWebhookEvent : WebhookEventBase { - public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) - : base(webhookFiringService, webHookService, webhookLogRepository, eventName) + public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) + : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.ContentDelete) { } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 1c82edcd27b5..9622ec805031 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -1,14 +1,15 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentPublishWebhookEvent : WebhookEventBase { - public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) - : base(webhookFiringService, webHookService, webhookLogRepository, eventName) + public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) + : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.ContentPublish) { } diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index f6c35425238f..bd94e4057a8d 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -1,14 +1,15 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentUnpublishWebhookEvent : WebhookEventBase { - public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) - : base(webhookFiringService, webHookService, webhookLogRepository, eventName) + public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) + : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.ContentUnpublish) { } diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index f43e2612f536..5f801f701f73 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -1,14 +1,15 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class MediaDeleteWebhookEvent : WebhookEventBase { - public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) - : base(webhookFiringService, webHookService, webhookLogRepository, eventName) + public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) + : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.MediaDelete) { } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index c1e66ca354ad..e87f68e4f62f 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -1,14 +1,15 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class MediaSaveWebhookEvent : WebhookEventBase { - public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) - : base(webhookFiringService, webHookService, webhookLogRepository, eventName) + public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) + : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.MediaSave) { } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 0d9c0c9c385a..61541081a778 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; @@ -14,12 +15,14 @@ public abstract class WebhookEventBase : IWebhookEvent, private readonly IWebhookFiringService _webhookFiringService; private readonly IWebHookService _webHookService; private readonly IWebhookLogRepository _webhookLogRepository; + private readonly ICoreScopeProvider _coreScopeProvider; - protected WebhookEventBase(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, string eventName) + protected WebhookEventBase(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider, string eventName) { _webhookFiringService = webhookFiringService; _webHookService = webHookService; _webhookLogRepository = webhookLogRepository; + _coreScopeProvider = coreScopeProvider; EventName = eventName; } @@ -48,8 +51,11 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc ResponseBody = await response.Content.ReadAsStringAsync(cancellationToken), StatusCode = response.StatusCode.ToString(), RetryCount = 0, + Key = Guid.NewGuid(), }; + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); await _webhookLogRepository.CreateAsync(log); + scope.Complete(); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs index 037703675642..5fa6112c1547 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; -[TableName(Constants.DatabaseSchema.Tables.Webhook)] +[TableName(Constants.DatabaseSchema.Tables.WebhookLog)] [PrimaryKey("id")] [ExplicitColumns] internal class WebhookLogDto diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs index 5e2655651073..5f0d795681d8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs @@ -17,4 +17,17 @@ public static WebhookLogDto CreateDto(WebhookLog log) => Key = log.Key, Id = log.Id, }; + + public static WebhookLog DtoToEntity(WebhookLogDto dto) => + new() + { + Date = dto.Date, + EventName = dto.EventName, + RequestBody = dto.RequestBody, + ResponseBody = dto.ResponseBody, + RetryCount = dto.RetryCount, + StatusCode = dto.StatusCode, + Key = dto.Key, + Id = dto.Id, + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs index 2f2474042f53..f8bc2acc8e1b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs @@ -1,9 +1,12 @@ -using Umbraco.Cms.Core.Models; +using NPoco; +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; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; @@ -21,5 +24,20 @@ public async Task CreateAsync(WebhookLog log) log.Id = id; } - public Task> GetPagedAsync(int skip, int take) => throw new NotImplementedException(); + public async Task> GetPagedAsync(int skip, int take) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From(); + + PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); + + Page? page = await _scopeAccessor.AmbientScope?.Database.PageAsync(pageNumber, pageSize, sql)!; + + return new PagedModel + { + Total = page.TotalItems, + Items = page.Items.Select(WebhookLogFactory.DtoToEntity), + }; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index de0f4d2d0cee..15fb2f7b61f6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -24,7 +24,7 @@ public async Task> GetAllAsync(int skip, int take) return new PagedModel { - Items = await DtosToEntities(webhookDtos), + Items = await DtosToEntities(webhookDtos.Skip(skip).Take(take)), Total = webhookDtos.Count, }; } From 8d7fc222e6c19156aef32c1c11fedf4c19e1bdb2 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Mon, 2 Oct 2023 14:36:29 +0200 Subject: [PATCH 060/102] Implement WebhookLogService --- .../DependencyInjection/UmbracoBuilder.cs | 1 + .../Services/IWebhookLogService.cs | 11 +++++ .../Services/WebhookLogService.cs | 33 +++++++++++++ .../Webhooks/ContentDeleteWebhookEvent.cs | 6 +-- .../Webhooks/ContentPublishWebhookEvent.cs | 6 +-- .../Webhooks/ContentUnpublishWebhookEvent.cs | 6 +-- .../Webhooks/MediaDeleteWebhookEvent.cs | 6 +-- .../Webhooks/MediaSaveWebhookEvent.cs | 6 +-- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 13 ++--- .../Implement/WebhookLogRepository.cs | 2 +- .../Controllers/WebHookController.cs | 6 +++ .../Services/WebhookLogServiceTests.cs | 49 +++++++++++++++++++ 12 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 src/Umbraco.Core/Services/IWebhookLogService.cs create mode 100644 src/Umbraco.Core/Services/WebhookLogService.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index e291fa7f17f1..abc89ee15b52 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -328,6 +328,7 @@ private void AddCoreServices() Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddNotificationAsyncHandler(); Services.AddNotificationAsyncHandler(); Services.AddNotificationAsyncHandler(); diff --git a/src/Umbraco.Core/Services/IWebhookLogService.cs b/src/Umbraco.Core/Services/IWebhookLogService.cs new file mode 100644 index 000000000000..12b53bfa7609 --- /dev/null +++ b/src/Umbraco.Core/Services/IWebhookLogService.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebhookLogService +{ + Task CreateAsync(WebhookLog webhookLog); + + Task> Get(int skip = 0, int take = int.MaxValue); +} diff --git a/src/Umbraco.Core/Services/WebhookLogService.cs b/src/Umbraco.Core/Services/WebhookLogService.cs new file mode 100644 index 000000000000..3b0bbebf1997 --- /dev/null +++ b/src/Umbraco.Core/Services/WebhookLogService.cs @@ -0,0 +1,33 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Services; + +public class WebhookLogService : IWebhookLogService +{ + private readonly IWebhookLogRepository _webhookLogRepository; + private readonly ICoreScopeProvider _coreScopeProvider; + + public WebhookLogService(IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) + { + _webhookLogRepository = webhookLogRepository; + _coreScopeProvider = coreScopeProvider; + } + + public async Task CreateAsync(WebhookLog webhookLog) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + await _webhookLogRepository.CreateAsync(webhookLog); + scope.Complete(); + + return webhookLog; + } + + public async Task> Get(int skip = 0, int take = int.MaxValue) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + return await _webhookLogRepository.GetPagedAsync(skip, take); + } +} diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index 90e198feb5cb..3aba563695b8 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -1,15 +1,13 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentDeleteWebhookEvent : WebhookEventBase { - public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) - : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.ContentDelete) + public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) + : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.ContentDelete) { } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 9622ec805031..883c32ba183d 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -1,15 +1,13 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentPublishWebhookEvent : WebhookEventBase { - public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) - : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.ContentPublish) + public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) + : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.ContentPublish) { } diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index bd94e4057a8d..3889422511d8 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -1,15 +1,13 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class ContentUnpublishWebhookEvent : WebhookEventBase { - public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) - : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.ContentUnpublish) + public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) + : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.ContentUnpublish) { } diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index 5f801f701f73..636eee335aec 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -1,15 +1,13 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class MediaDeleteWebhookEvent : WebhookEventBase { - public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) - : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.MediaDelete) + public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) + : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.MediaDelete) { } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index e87f68e4f62f..7fd49279cfc0 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -1,15 +1,13 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; public class MediaSaveWebhookEvent : WebhookEventBase { - public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) - : base(webhookFiringService, webHookService, webhookLogRepository, coreScopeProvider, Constants.WebhookEvents.MediaSave) + public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) + : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.MediaSave) { } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 61541081a778..419a46047ab6 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -1,7 +1,6 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -14,15 +13,13 @@ public abstract class WebhookEventBase : IWebhookEvent, private readonly IWebhookFiringService _webhookFiringService; private readonly IWebHookService _webHookService; - private readonly IWebhookLogRepository _webhookLogRepository; - private readonly ICoreScopeProvider _coreScopeProvider; + private readonly IWebhookLogService _webhookLogService; - protected WebhookEventBase(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider, string eventName) + protected WebhookEventBase(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, string eventName) { _webhookFiringService = webhookFiringService; _webHookService = webHookService; - _webhookLogRepository = webhookLogRepository; - _coreScopeProvider = coreScopeProvider; + _webhookLogService = webhookLogService; EventName = eventName; } @@ -53,9 +50,7 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc RetryCount = 0, Key = Guid.NewGuid(), }; - using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); - await _webhookLogRepository.CreateAsync(log); - scope.Complete(); + await _webhookLogService.CreateAsync(log); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs index f8bc2acc8e1b..30308b6c8c9a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs @@ -32,7 +32,7 @@ public async Task> GetPagedAsync(int skip, int take) PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); - Page? page = await _scopeAccessor.AmbientScope?.Database.PageAsync(pageNumber, pageSize, sql)!; + Page? page = await _scopeAccessor.AmbientScope?.Database.PageAsync(pageNumber + 1, pageSize, sql)!; return new PagedModel { diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs index 9a35f5326d91..196425972e94 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -72,4 +72,10 @@ public IActionResult GetEvents() List viewModels = _umbracoMapper.MapEnumerable(_webhookEventCollection.AsEnumerable()); return Ok(viewModels); } + + [HttpGet] + public IActionResult GetLogs() + { + return Ok(); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs new file mode 100644 index 000000000000..7716f83eda25 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs @@ -0,0 +1,49 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class WebhookLogServiceTests : UmbracoIntegrationTest +{ + private IWebhookLogService WebhookLogService => GetRequiredService(); + + [Test] + public async Task Can_Create_And_Get() + { + var createdWebhookLog = await WebhookLogService.CreateAsync(new WebhookLog + { + Date = DateTime.UtcNow, + EventName = Constants.WebhookEvents.ContentPublish, + RequestBody = "Test Request Body", + ResponseBody = "Test response body", + StatusCode = "200", + RetryCount = 0, + Key = Guid.NewGuid(), + }); + + + var webhookLogsPaged = await WebhookLogService.Get(); + + Assert.Multiple(() => + { + Assert.IsNotNull(webhookLogsPaged); + Assert.IsNotEmpty(webhookLogsPaged.Items); + Assert.AreEqual(1, webhookLogsPaged.Items.Count()); + var webHookLog = webhookLogsPaged.Items.First(); + Assert.AreEqual(createdWebhookLog.Date, webHookLog.Date); + Assert.AreEqual(createdWebhookLog.EventName, webHookLog.EventName); + Assert.AreEqual(createdWebhookLog.RequestBody, webHookLog.RequestBody); + Assert.AreEqual(createdWebhookLog.ResponseBody, webHookLog.ResponseBody); + Assert.AreEqual(createdWebhookLog.StatusCode, webHookLog.StatusCode); + Assert.AreEqual(createdWebhookLog.RetryCount, webHookLog.RetryCount); + Assert.AreEqual(createdWebhookLog.Key, webHookLog.Key); + }); + } +} From 4fd4b6a3c89cbd531214bc1957b4ac8e71c0ff38 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:38:32 +0200 Subject: [PATCH 061/102] Implement GetLogs --- .../Controllers/WebHookController.cs | 13 +++++++-- .../Mapping/WebhookMapDefinition.cs | 13 +++++++++ .../Models/WebhookLogViewModel.cs | 28 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs index 196425972e94..a8cf764cf37e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebHookController.cs @@ -13,12 +13,14 @@ public class WebHookController : UmbracoAuthorizedJsonController private readonly IWebHookService _webHookService; private readonly IUmbracoMapper _umbracoMapper; private readonly WebhookEventCollection _webhookEventCollection; + private readonly IWebhookLogService _webhookLogService; - public WebHookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection) + public WebHookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection, IWebhookLogService webhookLogService) { _webHookService = webHookService; _umbracoMapper = umbracoMapper; _webhookEventCollection = webhookEventCollection; + _webhookLogService = webhookLogService; } [HttpGet] @@ -74,8 +76,13 @@ public IActionResult GetEvents() } [HttpGet] - public IActionResult GetLogs() + public async Task GetLogs(int skip = 0, int take = int.MaxValue) { - return Ok(); + PagedModel logs = await _webhookLogService.Get(skip, take); + List mappedLogs = _umbracoMapper.MapEnumerable(logs.Items); + return Ok(new PagedResult(logs.Total, 0, 0) + { + Items = mappedLogs, + }); } } diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index afd61df1f998..9e6473ea3173 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -12,6 +12,7 @@ public void DefineMaps(IUmbracoMapper mapper) mapper.Define((_, _) => new Webhook(string.Empty), Map); mapper.Define((_, _) => new WebhookViewModel(), Map); mapper.Define((_, _) => new WebhookEventViewModel(), Map); + mapper.Define((_, _) => new WebhookLogViewModel(), Map); } // Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -Key -UpdateDate @@ -36,4 +37,16 @@ private void Map(Webhook source, WebhookViewModel target, MapperContext context) // Umbraco.Code.MapAll private void Map(IWebhookEvent source, WebhookEventViewModel target, MapperContext context) => target.EventName = source.EventName; + + // Umbraco.Code.MapAll + private void Map(WebhookLog source, WebhookLogViewModel target, MapperContext context) + { + target.Date = source.Date; + target.EventName = source.EventName; + target.Key = source.Key; + target.RequestBody = source.RequestBody; + target.ResponseBody = source.ResponseBody; + target.RetryCount = source.RetryCount; + target.StatusCode = source.StatusCode; + } } diff --git a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs new file mode 100644 index 000000000000..01444d2ee1cf --- /dev/null +++ b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs @@ -0,0 +1,28 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Web.Common.Models; + +[DataContract] +public class WebhookLogViewModel +{ + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "statusCode")] + public string StatusCode { get; set; } = string.Empty; + + [DataMember(Name = "date")] + public DateTime Date { get; set; } + + [DataMember(Name = "eventName")] + public string EventName { get; set; } = string.Empty; + + [DataMember(Name = "retryCount")] + public int RetryCount { get; set; } + + [DataMember(Name = "requestBody")] + public string RequestBody { get; set; } = string.Empty; + + [DataMember(Name = "responseBody")] + public string ResponseBody { get; set; } = string.Empty; +} From 9501346c9c3b5524f7ac2f34d331d6878ba1ca84 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:48:12 +0200 Subject: [PATCH 062/102] Add url to webhook log --- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 1 + src/Umbraco.Core/Webhooks/WebhookLog.cs | 4 ++++ src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs | 4 ++++ .../Persistence/Factories/WebhookLogFactory.cs | 2 ++ src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs | 1 + src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs | 3 +++ 6 files changed, 15 insertions(+) diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 419a46047ab6..44f6a1119373 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -49,6 +49,7 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc StatusCode = response.StatusCode.ToString(), RetryCount = 0, Key = Guid.NewGuid(), + Url = webhook.Url, }; await _webhookLogService.CreateAsync(log); } diff --git a/src/Umbraco.Core/Webhooks/WebhookLog.cs b/src/Umbraco.Core/Webhooks/WebhookLog.cs index 29495189b4b2..9635c5539217 100644 --- a/src/Umbraco.Core/Webhooks/WebhookLog.cs +++ b/src/Umbraco.Core/Webhooks/WebhookLog.cs @@ -3,7 +3,11 @@ public class WebhookLog { public int Id { get; set; } + public Guid Key { get; set; } + + public string Url { get; set; } = string.Empty; + public string StatusCode { get; set; } = string.Empty; public DateTime Date { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs index 5fa6112c1547..7c44f649d0fd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs @@ -25,6 +25,10 @@ internal class WebhookLogDto [NullSetting(NullSetting = NullSettings.NotNull)] public DateTime Date { get; set; } + [Column(Name = "url")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Url { get; set; } = string.Empty; + [Column(Name = "eventName")] [NullSetting(NullSetting = NullSettings.NotNull)] public string EventName { get; set; } = string.Empty; diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs index 5f0d795681d8..4748ed72c9a0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs @@ -16,6 +16,7 @@ public static WebhookLogDto CreateDto(WebhookLog log) => StatusCode = log.StatusCode, Key = log.Key, Id = log.Id, + Url = log.Url, }; public static WebhookLog DtoToEntity(WebhookLogDto dto) => @@ -29,5 +30,6 @@ public static WebhookLog DtoToEntity(WebhookLogDto dto) => StatusCode = dto.StatusCode, Key = dto.Key, Id = dto.Id, + Url = dto.Url }; } diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index 9e6473ea3173..84099e0c7f58 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -48,5 +48,6 @@ private void Map(WebhookLog source, WebhookLogViewModel target, MapperContext co target.ResponseBody = source.ResponseBody; target.RetryCount = source.RetryCount; target.StatusCode = source.StatusCode; + target.Url = source.Url; } } diff --git a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs index 01444d2ee1cf..852cf19fe36d 100644 --- a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs @@ -17,6 +17,9 @@ public class WebhookLogViewModel [DataMember(Name = "eventName")] public string EventName { get; set; } = string.Empty; + [DataMember(Name = "url")] + public string Url { get; set; } = string.Empty; + [DataMember(Name = "retryCount")] public int RetryCount { get; set; } From ece9bbbdc99aa1628e9c5eed19c8d6189fdf497c Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 3 Oct 2023 12:23:28 +0200 Subject: [PATCH 063/102] Implement log overview --- .../src/common/resources/webhooks.resource.js | 8 +++- .../src/views/webhooks/logs.controller.js | 29 +++++++++++++ .../src/views/webhooks/logs.html | 43 ++++++++++++++++++- .../src/views/webhooks/webhooks.controller.js | 3 -- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js index 690fadf1f1d4..f820358627d2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js @@ -35,7 +35,13 @@ $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetEvents')), 'Failed to get events' ); - } + }, + getLogs(skip, take) { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetLogs', {skip, take})), + 'Failed to get logs' + ); + }, }; } angular.module('umbraco.resources').factory('webhooksResource', webhooksResource); 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 new file mode 100644 index 000000000000..6e44b33749c5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js @@ -0,0 +1,29 @@ +(function () { + "use strict"; + + function WebhookController($q,$scope, webhooksResource, notificationsService, editorService, overlayService, contentTypeResource, mediaTypeResource) { + var vm = this; + vm.logs = []; + vm.openLogOverlay = openLogOverlay; + vm.isChecked = isChecked; + + function loadLogs (){ + return webhooksResource.getLogs() + .then((data) => { + vm.logs = data.items; + }); + } + + function openLogOverlay (log) { + } + + function isChecked (log) { + return log.retryCount < 5; + } + + loadLogs(); + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.WebhookLogController", WebhookController); + +})(); 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 f8bb770b260d..59f3b8987d43 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html @@ -1 +1,42 @@ -

Test

+
+ + + + + + + + + + + + + + + + + + + + +
DateUrlEventRetryCount
+ + + + {{ log.date}} + + {{ log.url }} + + {{ log.eventName }} + + {{ log.retryCount}} + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js index 1e2f8f5faefb..8eb2b878b55c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js @@ -20,8 +20,6 @@ return webhooksResource.getAllEvents() .then((data) => { vm.events = data.map(item => item.eventName); - console.log("logging vm.events") - console.log(vm.events) }); } @@ -108,7 +106,6 @@ if(isCreating){ webhooksResource.create(model.webhook) .then(() => { - console.log("Loading freaking webhooks") loadWebhooks() notificationsService.success('Webhook saved.'); editorService.close(); From 2ef704245b17f4e560eba6683f9fd07410709ee1 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 4 Oct 2023 09:29:18 +0200 Subject: [PATCH 064/102] Formatting --- .../src/common/resources/webhooks.resource.js | 2 +- .../Umbraco.Core/Services/WebhookLogServiceTests.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js index f820358627d2..3611e67de965 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js @@ -41,7 +41,7 @@ $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetLogs', {skip, take})), 'Failed to get logs' ); - }, + } }; } angular.module('umbraco.resources').factory('webhooksResource', webhooksResource); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs index 7716f83eda25..c9a0da21c860 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs @@ -1,6 +1,5 @@ using NUnit.Framework; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Tests.Common.Testing; From e67ab160507dd3946d803c377742a012ac46b856 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 4 Oct 2023 11:25:24 +0200 Subject: [PATCH 065/102] Implement details view --- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 2 + src/Umbraco.Core/Webhooks/WebhookLog.cs | 4 ++ .../Persistence/Dtos/WebhookLogDto.cs | 8 ++++ .../Factories/WebhookLogFactory.cs | 6 ++- .../Mapping/WebhookMapDefinition.cs | 2 + .../Models/WebhookLogViewModel.cs | 6 +++ .../src/views/webhooks/logs.controller.js | 14 ++++++- .../src/views/webhooks/overlays/details.html | 39 +++++++++++++++++++ src/Umbraco.Web.UI/TestController.cs | 9 +++++ 9 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html create mode 100644 src/Umbraco.Web.UI/TestController.cs diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 44f6a1119373..d64c83234def 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -50,6 +50,8 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc RetryCount = 0, Key = Guid.NewGuid(), Url = webhook.Url, + ResponseHeaders = response.Headers.ToString(), + RequestHeaders = response.RequestMessage.Headers.ToString(), }; await _webhookLogService.CreateAsync(log); } diff --git a/src/Umbraco.Core/Webhooks/WebhookLog.cs b/src/Umbraco.Core/Webhooks/WebhookLog.cs index 9635c5539217..04b25e3873ab 100644 --- a/src/Umbraco.Core/Webhooks/WebhookLog.cs +++ b/src/Umbraco.Core/Webhooks/WebhookLog.cs @@ -16,7 +16,11 @@ public class WebhookLog public int RetryCount { get; set; } + public string RequestHeaders { get; set; } = string.Empty; + public string RequestBody { get; set; } = string.Empty; + public string ResponseHeaders { get; set; } = string.Empty; + public string ResponseBody { get; set; } = string.Empty; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs index 7c44f649d0fd..e3b45eebce40 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs @@ -37,10 +37,18 @@ internal class WebhookLogDto [NullSetting(NullSetting = NullSettings.NotNull)] public int RetryCount { get; set; } + [Column(Name = "requestHeaders")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string RequestHeaders { get; set; } = string.Empty; + [Column(Name = "requestBody")] [NullSetting(NullSetting = NullSettings.NotNull)] public string RequestBody { get; set; } = string.Empty; + [Column(Name = "responseHeaders")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string ResponseHeaders { get; set; } = string.Empty; + [Column(Name = "responseBody")] [NullSetting(NullSetting = NullSettings.NotNull)] public string ResponseBody { get; set; } = string.Empty; diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs index 4748ed72c9a0..687b08c61424 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs @@ -17,6 +17,8 @@ public static WebhookLogDto CreateDto(WebhookLog log) => Key = log.Key, Id = log.Id, Url = log.Url, + RequestHeaders = log.RequestHeaders, + ResponseHeaders = log.ResponseHeaders, }; public static WebhookLog DtoToEntity(WebhookLogDto dto) => @@ -30,6 +32,8 @@ public static WebhookLog DtoToEntity(WebhookLogDto dto) => StatusCode = dto.StatusCode, Key = dto.Key, Id = dto.Id, - Url = dto.Url + Url = dto.Url, + RequestHeaders = dto.RequestHeaders, + ResponseHeaders = dto.ResponseHeaders, }; } diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index 84099e0c7f58..af22efcc4783 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -49,5 +49,7 @@ private void Map(WebhookLog source, WebhookLogViewModel target, MapperContext co target.RetryCount = source.RetryCount; target.StatusCode = source.StatusCode; target.Url = source.Url; + target.RequestHeaders = source.RequestHeaders; + target.ResponseHeaders = source.ResponseHeaders; } } diff --git a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs index 852cf19fe36d..999ec95df685 100644 --- a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs @@ -23,9 +23,15 @@ public class WebhookLogViewModel [DataMember(Name = "retryCount")] public int RetryCount { get; set; } + [DataMember(Name = "requestHeaders")] + public string RequestHeaders { get; set; } = string.Empty; + [DataMember(Name = "requestBody")] public string RequestBody { get; set; } = string.Empty; + [DataMember(Name = "responseHeaders")] + public string ResponseHeaders { get; set; } = string.Empty; + [DataMember(Name = "responseBody")] public string ResponseBody { get; set; } = string.Empty; } 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 6e44b33749c5..988c90dece19 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 @@ -1,7 +1,7 @@ (function () { "use strict"; - function WebhookController($q,$scope, webhooksResource, notificationsService, editorService, overlayService, contentTypeResource, mediaTypeResource) { + function WebhookLogController($q,$scope, webhooksResource, notificationsService, overlayService) { var vm = this; vm.logs = []; vm.openLogOverlay = openLogOverlay; @@ -15,6 +15,16 @@ } function openLogOverlay (log) { + overlayService.open({ + view: "views/webhooks/overlays/details.html", + title: 'Details', + position: 'right', + log, + currentUser: this.currentUser, + close: () => { + overlayService.close(); + }, + }); } function isChecked (log) { @@ -24,6 +34,6 @@ loadLogs(); } - angular.module("umbraco").controller("Umbraco.Editors.Webhooks.WebhookLogController", WebhookController); + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.WebhookLogController", WebhookLogController); })(); 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 new file mode 100644 index 000000000000..b849dc179d7c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html @@ -0,0 +1,39 @@ +
+
+
+
+ + +
+
{{model.webhookLogEntry.response.statusDescription}} ({{model.webhookLogEntry.response.statusCode}})
+
+
+
+ Date +
{{model.log.date}}
+
+
+ Url +
{{model.log.url}}
+
+
+ Event +
{{model.log.eventName}}
+
+
+ Retry count +
{{model.log.retryCount}}
+
+
+ Request +
{{model.log.requestHeaders}}
+---
+{{model.log.requestBody}}
+
+
+ Response +
{{model.log.responseHeaders}}
+---
+{{model.log.responseBody}}
+
+
diff --git a/src/Umbraco.Web.UI/TestController.cs b/src/Umbraco.Web.UI/TestController.cs new file mode 100644 index 000000000000..d48423288d1f --- /dev/null +++ b/src/Umbraco.Web.UI/TestController.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Web.Common.Controllers; + +namespace Umbraco.Cms.Web.UI; + +public class TestController : UmbracoApiController +{ + public IActionResult GetAllProducts() => Ok(); +} From 8fda54a1a033ca635448ffc523f371cffc67cb41 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 4 Oct 2023 13:54:03 +0200 Subject: [PATCH 066/102] Refactor to get actual retry count --- .../Models/WebhookResponseModel.cs | 8 ++++++ src/Umbraco.Core/Webhooks/IRetryService.cs | 6 +++-- .../Webhooks/IWebhookFiringService.cs | 6 +++-- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 14 +++++------ .../Services/Implement/RetryService.cs | 18 ++++++++++--- .../Implement/WebhookFiringService.cs | 25 ++++++++++--------- 6 files changed, 50 insertions(+), 27 deletions(-) create mode 100644 src/Umbraco.Core/Models/WebhookResponseModel.cs diff --git a/src/Umbraco.Core/Models/WebhookResponseModel.cs b/src/Umbraco.Core/Models/WebhookResponseModel.cs new file mode 100644 index 000000000000..321ebdaa9288 --- /dev/null +++ b/src/Umbraco.Core/Models/WebhookResponseModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Models; + +public class WebhookResponseModel +{ + public HttpResponseMessage HttpResponseMessage { get; set; } = null!; + + public int RetryCount { get; set; } +} diff --git a/src/Umbraco.Core/Webhooks/IRetryService.cs b/src/Umbraco.Core/Webhooks/IRetryService.cs index 56e0ebe0e1f8..ebbc5d10460e 100644 --- a/src/Umbraco.Core/Webhooks/IRetryService.cs +++ b/src/Umbraco.Core/Webhooks/IRetryService.cs @@ -1,6 +1,8 @@ -namespace Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Webhooks; public interface IRetryService { - Task RetryAsync(Func> action, int maxRetries = 3, TimeSpan? retryDelay = null); + Task RetryAsync(Func> action, int maxRetries = 3, TimeSpan? retryDelay = null); } diff --git a/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs b/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs index a161bb2b8ea9..9c27e12efff1 100644 --- a/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs +++ b/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs @@ -1,6 +1,8 @@ -namespace Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Webhooks; public interface IWebhookFiringService { - Task Fire( string url, string eventName, object? requestObject); + Task Fire( string url, string eventName, object? requestObject); } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index d64c83234def..380415e42dd8 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -38,20 +38,20 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc continue; } - HttpResponseMessage response = await _webhookFiringService.Fire(webhook.Url, EventName, entity); + WebhookResponseModel response = await _webhookFiringService.Fire(webhook.Url, EventName, entity); var log = new WebhookLog { Date = DateTime.UtcNow, EventName = EventName, - RequestBody = await response.RequestMessage!.Content!.ReadAsStringAsync(cancellationToken), - ResponseBody = await response.Content.ReadAsStringAsync(cancellationToken), - StatusCode = response.StatusCode.ToString(), - RetryCount = 0, + RequestBody = await response.HttpResponseMessage.RequestMessage!.Content!.ReadAsStringAsync(cancellationToken), + ResponseBody = await response.HttpResponseMessage.Content.ReadAsStringAsync(cancellationToken), + StatusCode = response.HttpResponseMessage.StatusCode.ToString(), + RetryCount = response.RetryCount, Key = Guid.NewGuid(), Url = webhook.Url, - ResponseHeaders = response.Headers.ToString(), - RequestHeaders = response.RequestMessage.Headers.ToString(), + ResponseHeaders = response.HttpResponseMessage.Headers.ToString(), + RequestHeaders = response.HttpResponseMessage.RequestMessage.Headers.ToString(), }; await _webhookLogService.CreateAsync(log); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs b/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs index 868798b9fd9d..9bffa2eaf6fe 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs @@ -1,16 +1,22 @@ -using Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; namespace Umbraco.Cms.Infrastructure.Services.Implement; public class RetryService : IRetryService { - public async Task RetryAsync(Func> action, int maxRetries = 3, TimeSpan? retryDelay = null) + public async Task RetryAsync(Func> action, int maxRetries = 5, TimeSpan? retryDelay = null) { for (int retry = 0; retry < maxRetries; retry++) { try { - return await action(); + HttpResponseMessage response = await action(); + return new WebhookResponseModel + { + RetryCount = retry, + HttpResponseMessage = response, + }; } catch (Exception ex) { @@ -22,7 +28,11 @@ public async Task RetryAsync(Func } } - return null!; + return new WebhookResponseModel + { + RetryCount = maxRetries, + HttpResponseMessage = null!, + }; // TODO: Every retry failed, should we log some errors here, maybe the error in the catch? } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs index bdb1b76d198f..a19c83184134 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs @@ -8,7 +8,7 @@ public class WebhookFiringService : IWebhookFiringService { private readonly IJsonSerializer _jsonSerializer; private readonly IRetryService _retryService; - private readonly int _maxRetries = 3; + private readonly int _maxRetries = 5; public WebhookFiringService(IJsonSerializer jsonSerializer, IRetryService retryService) { @@ -16,17 +16,18 @@ public WebhookFiringService(IJsonSerializer jsonSerializer, IRetryService retryS _retryService = retryService; } - public async Task Fire(string url, string eventName, object? requestObject) => await _retryService.RetryAsync( - async () => - { - using var httpClient = new HttpClient(); + public async Task Fire(string url, string eventName, object? requestObject) => + await _retryService.RetryAsync( + async () => + { + using var httpClient = new HttpClient(); - var myContent = _jsonSerializer.Serialize(requestObject); - var buffer = System.Text.Encoding.UTF8.GetBytes(myContent); - var byteContent = new ByteArrayContent(buffer); - httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Umb-Webhook-Event", eventName); + var myContent = _jsonSerializer.Serialize(requestObject); + var buffer = System.Text.Encoding.UTF8.GetBytes(myContent); + var byteContent = new ByteArrayContent(buffer); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Umb-Webhook-Event", eventName); - return await httpClient.PostAsync(url, byteContent); - }, - _maxRetries); + return await httpClient.PostAsync(url, byteContent); + }, + _maxRetries); } From 49250c72a0c4ae030d8808a646447a9a50feec59 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:54:25 +0200 Subject: [PATCH 067/102] Refactor firing to fire only when Enabled --- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 7 ++++++- .../Repositories/Implement/WebhookRepository.cs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 380415e42dd8..4840d9045dad 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -33,7 +33,12 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc { foreach (TEntity entity in GetEntitiesFromNotification(notification)) { - if (!webhook.EntityKeys.Contains(entity.ContentType.Key)) + if (webhook.EntityKeys.Any() && !webhook.EntityKeys.Contains(entity.ContentType.Key)) + { + continue; + } + + if (!webhook.Enabled) { continue; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index 15fb2f7b61f6..d464b0b5e6bb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -64,7 +64,7 @@ public async Task CreateAsync(Webhook webhook) public async Task> GetByEventNameAsync(string eventName) { Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() - .Select() + .SelectAll() .From() .InnerJoin() .On(left => left.Id, right => right.WebhookId) From 24a5fa711467eef58a12e4f1dbb357913707d093 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Fri, 6 Oct 2023 13:57:06 +0200 Subject: [PATCH 068/102] Add Status code to detailed view --- .../src/views/webhooks/overlays/details.html | 4 ++++ src/Umbraco.Web.UI/TestController.cs | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 src/Umbraco.Web.UI/TestController.cs 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 b849dc179d7c..9c0856cb676a 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 @@ -16,6 +16,10 @@ Url
{{model.log.url}}
+
+ Status Code +
{{model.log.statusCode}}
+
Event
{{model.log.eventName}}
diff --git a/src/Umbraco.Web.UI/TestController.cs b/src/Umbraco.Web.UI/TestController.cs deleted file mode 100644 index d48423288d1f..000000000000 --- a/src/Umbraco.Web.UI/TestController.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Web.Common.Controllers; - -namespace Umbraco.Cms.Web.UI; - -public class TestController : UmbracoApiController -{ - public IActionResult GetAllProducts() => Ok(); -} From 3f7de8868ef87307102151ad1167fcd2cdbfa9af Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:41:12 +0200 Subject: [PATCH 069/102] Add configuration to disable webhooks entirely --- .../Configuration/Models/WebhookSettings.cs | 22 +++++++++++++++++++ src/Umbraco.Core/Constants-Configuration.cs | 1 + .../UmbracoBuilder.Configuration.cs | 3 ++- .../Webhooks/ContentDeleteWebhookEvent.cs | 8 ++++--- .../Webhooks/ContentPublishWebhookEvent.cs | 8 ++++--- .../Webhooks/ContentUnpublishWebhookEvent.cs | 8 ++++--- .../Webhooks/MediaDeleteWebhookEvent.cs | 8 ++++--- .../Webhooks/MediaSaveWebhookEvent.cs | 8 ++++--- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 20 ++++++++++++++--- tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs | 2 ++ 10 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 src/Umbraco.Core/Configuration/Models/WebhookSettings.cs diff --git a/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs b/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs new file mode 100644 index 000000000000..b0de4eca3582 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs @@ -0,0 +1,22 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigWebhook)] +public class WebhookSettings +{ + private const bool StaticEnabled = true; + + /// + /// Gets or sets a value indicating whether webhooks are enabled. + /// + /// + /// + /// By default, webhooks are enabled. + /// If this option is set to false webhooks will no longer send web-requests. + /// the Run level. + /// + /// + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 4f9d045cb620..d29ee7019fee 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -64,6 +64,7 @@ public static class Configuration public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; + public const string ConfigWebhook = ConfigPrefix + "Webhook"; public static class NamedOptions { diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 411c39b178c1..e09e731956eb 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -87,7 +87,8 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder) .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); // Configure connection string and ensure it's updated when the configuration changes builder.Services.AddSingleton, ConfigureConnectionStrings>(); diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index 3aba563695b8..be12f30d3910 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core.Models; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; @@ -6,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentDeleteWebhookEvent : WebhookEventBase { - public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) - : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.ContentDelete) + public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.ContentDelete) { } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 883c32ba183d..737d2397cf99 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core.Models; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; @@ -6,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentPublishWebhookEvent : WebhookEventBase { - public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) - : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.ContentPublish) + public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.ContentPublish) { } diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index 3889422511d8..4987c897d6b9 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core.Models; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; @@ -6,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentUnpublishWebhookEvent : WebhookEventBase { - public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) - : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.ContentUnpublish) + public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.ContentUnpublish) { } diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index 636eee335aec..71203091a0d4 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core.Models; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; @@ -6,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class MediaDeleteWebhookEvent : WebhookEventBase { - public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) - : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.MediaDelete) + public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.MediaDelete) { } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index 7fd49279cfc0..20f14f4c475b 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core.Models; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; @@ -6,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class MediaSaveWebhookEvent : WebhookEventBase { - public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService) - : base(webhookFiringService, webHookService, webhookLogService, Constants.WebhookEvents.MediaSave) + public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.MediaSave) { } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 4840d9045dad..bfc44c05a0ed 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -1,7 +1,8 @@ -using Umbraco.Cms.Core.Events; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Webhooks; @@ -14,19 +15,32 @@ public abstract class WebhookEventBase : IWebhookEvent, private readonly IWebhookFiringService _webhookFiringService; private readonly IWebHookService _webHookService; private readonly IWebhookLogService _webhookLogService; + private WebhookSettings _webhookSettings; - protected WebhookEventBase(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, string eventName) + protected WebhookEventBase( + IWebhookFiringService webhookFiringService, + IWebHookService webHookService, + IWebhookLogService webhookLogService, + IOptionsMonitor webhookSettings, + string eventName) { _webhookFiringService = webhookFiringService; _webHookService = webHookService; _webhookLogService = webhookLogService; EventName = eventName; + _webhookSettings = webhookSettings.CurrentValue; + webhookSettings.OnChange(x => _webhookSettings = x); } public string EventName { get; set; } public async Task HandleAsync(TNotification notification, CancellationToken cancellationToken) { + if (_webhookSettings.Enabled is false) + { + return; + } + IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); foreach (Webhook webhook in webhooks) diff --git a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs index 3ef955afb8f5..a289a45ec320 100644 --- a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs +++ b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs @@ -80,5 +80,7 @@ public class UmbracoCmsDefinition public DataTypesSettings DataTypes { get; set; } = null!; public MarketplaceSettings Marketplace { get; set; } = null!; + + public WebhookSettings Webhook { get; set; } = null!; } } From 8ceab7b8822f90b37c635ba0bcc867f2829b3092 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 11 Oct 2023 09:52:54 +0200 Subject: [PATCH 070/102] Implement custom headers frontend --- src/Umbraco.Core/Models/Webhook.cs | 21 ++++++- .../Persistence/Dtos/WebhookDto.cs | 2 + .../Mapping/WebhookMapDefinition.cs | 2 + .../Models/WebhookViewModel.cs | 8 +++ .../src/views/webhooks/logs.controller.js | 2 +- .../webhooks/overlays/edit.controller.js | 23 +++++++ .../src/views/webhooks/overlays/edit.html | 51 ++++++++++++++-- .../webhooks/overlays/header.controller.js | 21 +++++++ .../src/views/webhooks/overlays/header.html | 61 +++++++++++++++++++ .../src/views/webhooks/webhooks.controller.js | 8 +-- 10 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.html diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index f305071d4a73..ff1ce38f2c4b 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -9,6 +9,7 @@ public class Webhook : EntityBase private string[] _events; private Guid[] _entityKeys; private bool _enabled; + private Dictionary _headers; // Custom comparer for enumerable guids private static readonly DelegateEqualityComparer> _guidEnumerableComparer = @@ -22,9 +23,21 @@ public class Webhook : EntityBase (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), enum1 => enum1.GetHashCode()); - public Webhook(string url, bool? enabled = null, Guid[]? entityKeys = null, string[]? events = null) + // Custom comparer for enumerable webhook events + private static readonly DelegateEqualityComparer> _dictionaryEnumerableComparer = + new( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); + + public Webhook(string url, bool? enabled = null, Guid[]? entityKeys = null, string[]? events = null, Dictionary? headers = null) { _url = url; + _headers = headers ?? new Dictionary + { + {"HeaderOne", "HeaderOneValue"}, + {"HeaderTwo", "HeaderTwoValue"}, + {"HeaderThree", "HeaderThreeValue"}, + }; _events = events ?? Array.Empty(); _entityKeys = entityKeys ?? Array.Empty(); _enabled = enabled ?? false; @@ -53,4 +66,10 @@ public bool Enabled get => _enabled; set => SetPropertyValueAndDetectChanges(value, ref _enabled, nameof(Enabled)); } + + public Dictionary Headers + { + get => _headers; + set => SetPropertyValueAndDetectChanges(value, ref _headers!, nameof(Headers), _dictionaryEnumerableComparer); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs index febdbaba1c77..e459b74c0f61 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -32,5 +32,7 @@ internal class WebhookDto [ResultColumn] [Reference(ReferenceType.Many, ReferenceMemberName = nameof(EntityKey2WebhookDto.WebhookId))] public List EntityKey2WebhookDtos { get; set; } = new(); + + } diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index af22efcc4783..94240994d8f3 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -23,6 +23,7 @@ private void Map(WebhookViewModel source, Webhook target, MapperContext context) target.Url = source.Url; target.Enabled = source.Enabled; target.Key = source.Key ?? Guid.NewGuid(); + target.Headers = source.Headers; } // Umbraco.Code.MapAll @@ -33,6 +34,7 @@ private void Map(Webhook source, WebhookViewModel target, MapperContext context) target.Url = source.Url; target.Enabled = source.Enabled; target.Key = source.Key; + target.Headers = source.Headers; } // Umbraco.Code.MapAll diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs index 820964baf988..d0f8f12207c2 100644 --- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -20,4 +20,12 @@ public class WebhookViewModel [DataMember(Name = "enabled")] public bool Enabled { get; set; } + + [DataMember(Name = "headers")] + public Dictionary Headers { get; set; } = new() + { + {"HeaderOne", "HeaderOneValue"}, + {"HeaderTwo", "HeaderTwoValue"}, + {"HeaderThree", "HeaderThreeValue"}, + }; } 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 988c90dece19..c8cc598eb65d 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 @@ -23,7 +23,7 @@ currentUser: this.currentUser, close: () => { overlayService.close(); - }, + } }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js index 5ef198d84026..09efa64ea614 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js @@ -4,6 +4,8 @@ var vm = this; vm.clearContentType = clearContentType; vm.clearEvent = clearEvent; + vm.removeHeader = removeHeader; + vm.openCreateHeader = openCreateHeader; this.openEventPicker = () => { editorService.eventPicker({ title: "Select event", @@ -34,6 +36,22 @@ }); }; + function openCreateHeader() { + editorService.open({ + title: "Create header", + view: "views/webhooks/overlays/header.html", + size: 'small', + position: 'right', + submit(model) { + $scope.model.webhook.headers[model.key] = model.value; + editorService.close(); + }, + close() { + editorService.close(); + } + }); + } + function getEntities(selection, isContent) { const resource = isContent ? contentTypeResource : mediaTypeResource; $scope.model.contentTypes = []; @@ -66,6 +84,11 @@ } } + function removeHeader(key) { + delete $scope.model.webhook.headers[key]; + } + + this.close = () => { if ($scope.model.close) { $scope.model.close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html index 9e4e7707d78b..e9287792e84b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html @@ -13,7 +13,8 @@
- + + required/> - + - - + + + + + + + + + + + + + + + + +
NameValue
+ {{ key }} + + {{ value }} + + + +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.controller.js new file mode 100644 index 000000000000..a77a4f5c0065 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.controller.js @@ -0,0 +1,21 @@ +(function () { + "use strict"; + function HeaderController($scope) { + var vm = this; + $scope.headerModel = { key: "", value: "" }; + vm.submit = submit; + vm.close = close; + + function submit () { + if ($scope.headerModel.key && $scope.headerModel.value) { + $scope.model.submit($scope.headerModel); + } + } + + function close () { + $scope.model.close(); + } + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.HeaderController", HeaderController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.html new file mode 100644 index 000000000000..b9adcb2bf6fe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.html @@ -0,0 +1,61 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js index 8eb2b878b55c..40dae16e6c93 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js @@ -84,13 +84,7 @@ view: "views/webhooks/overlays/edit.html", events: vm.events, contentTypes : webhook ? getEntities(webhook) : null, - webhook: webhook ? { - entityKeys: webhook.entityKeys, - enabled: webhook.enabled, - events: webhook.events, - key: webhook.key, - url: webhook.url - } : {enabled: true}, + webhook: webhook ? webhook : {enabled: true}, submit: (model) => { model.disableSubmitButton = true; model.submitButtonState = 'busy'; From cf962a92328e235ba688624166915a7b70275976 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 12 Oct 2023 10:29:24 +0200 Subject: [PATCH 071/102] Implement persistence of custom headers --- src/Umbraco.Core/Models/Webhook.cs | 7 +------ .../Persistence/Constants-DatabaseSchema.cs | 1 + src/Umbraco.Core/Services/WebhookService.cs | 1 + .../Webhooks/IWebhookFiringService.cs | 2 +- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 2 +- .../Install/DatabaseSchemaCreator.cs | 1 + .../Persistence/Dtos/Headers2WebhookDto.cs | 21 +++++++++++++++++++ .../Persistence/Dtos/WebhookDto.cs | 4 +++- .../Persistence/Factories/WebhookFactory.cs | 13 ++++++++++-- .../Implement/WebhookRepository.cs | 10 ++++++--- .../Implement/WebhookFiringService.cs | 8 +++++-- .../Models/WebhookViewModel.cs | 7 +------ .../webhooks/overlays/edit.controller.js | 3 +++ 13 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/Headers2WebhookDto.cs diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs index ff1ce38f2c4b..f7201a2b828b 100644 --- a/src/Umbraco.Core/Models/Webhook.cs +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -32,12 +32,7 @@ public class Webhook : EntityBase public Webhook(string url, bool? enabled = null, Guid[]? entityKeys = null, string[]? events = null, Dictionary? headers = null) { _url = url; - _headers = headers ?? new Dictionary - { - {"HeaderOne", "HeaderOneValue"}, - {"HeaderTwo", "HeaderTwoValue"}, - {"HeaderThree", "HeaderThreeValue"}, - }; + _headers = headers ?? new Dictionary(); _events = events ?? Array.Empty(); _entityKeys = entityKeys ?? Array.Empty(); _enabled = enabled ?? false; diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 679561940524..aaa6c77b0108 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -89,6 +89,7 @@ public static class Tables public const string Webhook = TableNamePrefix + "Webhook"; public const string EntityKey2Webhook = TableNamePrefix + "EntityKey2Webhook"; public const string Event2Webhook = TableNamePrefix + "Event2Webhook"; + public const string Headers2Webhook = TableNamePrefix + "Headers2Webhook"; public const string WebhookLog = TableNamePrefix + "WebhookLog"; } } diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 0ffed52f2af3..32fff1cea049 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -41,6 +41,7 @@ public async Task UpdateAsync(Webhook updateModel) currentWebhook.EntityKeys = updateModel.EntityKeys; currentWebhook.Events = updateModel.Events; currentWebhook.Url = updateModel.Url; + currentWebhook.Headers = updateModel.Headers; await _webhookRepository.UpdateAsync(currentWebhook); scope.Complete(); diff --git a/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs b/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs index 9c27e12efff1..38bd8b266e59 100644 --- a/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs +++ b/src/Umbraco.Core/Webhooks/IWebhookFiringService.cs @@ -4,5 +4,5 @@ namespace Umbraco.Cms.Core.Webhooks; public interface IWebhookFiringService { - Task Fire( string url, string eventName, object? requestObject); + Task Fire(Webhook webhook, string eventName, object? requestObject); } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index bfc44c05a0ed..5c6f1ed35e66 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -57,7 +57,7 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc continue; } - WebhookResponseModel response = await _webhookFiringService.Fire(webhook.Url, EventName, entity); + WebhookResponseModel response = await _webhookFiringService.Fire(webhook, EventName, entity); var log = new WebhookLog { diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 2351e026c0fc..b174cacd2d9a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -86,6 +86,7 @@ public class DatabaseSchemaCreator typeof(WebhookDto), typeof(EntityKey2WebhookDto), typeof(Event2WebhookDto), + typeof(Headers2WebhookDto), typeof(WebhookLogDto), }; diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/Headers2WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/Headers2WebhookDto.cs new file mode 100644 index 000000000000..2a20f8f720cb --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/Headers2WebhookDto.cs @@ -0,0 +1,21 @@ +using System.Data; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Headers2Webhook)] +public class Headers2WebhookDto +{ + [Column("webhookId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_heaeders2WebhookDto", OnColumns = "webhookId, key")] + [ForeignKey(typeof(WebhookDto), OnDelete = Rule.Cascade)] + public int WebhookId { get; set; } + + [Column("Key")] + public string Key { get; set; } = string.Empty; + + [Column("Value")] + public string Value { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs index e459b74c0f61..f8c82744a621 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -33,6 +33,8 @@ internal class WebhookDto [Reference(ReferenceType.Many, ReferenceMemberName = nameof(EntityKey2WebhookDto.WebhookId))] public List EntityKey2WebhookDtos { get; set; } = new(); - + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = nameof(Headers2WebhookDto.WebhookId))] + public List Headers2WebhookDtos { get; set; } = new(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs index a619075b8fd7..882959d5559a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -5,13 +5,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class WebhookFactory { - public static Webhook BuildEntity(WebhookDto dto, IEnumerable? entityKey2WebhookDtos = null, IEnumerable? event2WebhookDtos = null) + public static Webhook BuildEntity(WebhookDto dto, IEnumerable? entityKey2WebhookDtos = null, IEnumerable? event2WebhookDtos = null, IEnumerable? headersWebhookDtos = null) { var entity = new Webhook( dto.Url, dto.Enabled, entityKey2WebhookDtos?.Select(x => x.EntityKey).ToArray(), - event2WebhookDtos?.Select(x => x.Event).ToArray()); + event2WebhookDtos?.Select(x => x.Event).ToArray(), + headersWebhookDtos?.ToDictionary(x => x.Key, x => x.Value)); try { @@ -59,4 +60,12 @@ public static IEnumerable BuildEvent2WebhookDto(Webhook webhoo Event = x, WebhookId = webhook.Id, }); + + public static IEnumerable BuildHeaders2WebhookDtos(Webhook webhook) => + webhook.Headers.Select(x => new Headers2WebhookDto + { + Key = x.Key, + Value = x.Value, + WebhookId = webhook.Id, + }); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index d464b0b5e6bb..403d9f0fb5ba 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -40,9 +40,9 @@ public async Task CreateAsync(Webhook webhook) var id = Convert.ToInt32(result); webhook.Id = id; - IEnumerable entityKeys = WebhookFactory.BuildEvent2WebhookDto(webhook); - await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(entityKeys)!; + await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(WebhookFactory.BuildEvent2WebhookDto(webhook))!; await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(WebhookFactory.BuildEntityKey2WebhookDto(webhook))!; + await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(WebhookFactory.BuildHeaders2WebhookDtos(webhook))!; webhook.ResetDirtyProperties(); @@ -108,15 +108,18 @@ private void DeleteManyToOneReferences(int webhookId) { _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); + _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); } private void InsertManyToOneReferences(Webhook webhook) { IEnumerable buildEntityKey2WebhookDtos = WebhookFactory.BuildEntityKey2WebhookDto(webhook); IEnumerable buildEvent2WebhookDtos = WebhookFactory.BuildEvent2WebhookDto(webhook); + IEnumerable header2WebhookDtos = WebhookFactory.BuildHeaders2WebhookDtos(webhook); _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(buildEntityKey2WebhookDtos); _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(buildEvent2WebhookDtos); + _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(header2WebhookDtos); } private async Task> DtosToEntities(IEnumerable dtos) @@ -135,7 +138,8 @@ 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 })!; - Webhook entity = WebhookFactory.BuildEntity(dto, webhookEntityKeyDtos, event2WebhookDtos); + List? headersWebhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync("WHERE webhookId = @webhookId", new { webhookId = dto.Id })!; + Webhook entity = WebhookFactory.BuildEntity(dto, webhookEntityKeyDtos, event2WebhookDtos, headersWebhookDtos); // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); diff --git a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs index a19c83184134..7300986745d6 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs @@ -16,7 +16,7 @@ public WebhookFiringService(IJsonSerializer jsonSerializer, IRetryService retryS _retryService = retryService; } - public async Task Fire(string url, string eventName, object? requestObject) => + public async Task Fire(Webhook webhook, string eventName, object? requestObject) => await _retryService.RetryAsync( async () => { @@ -26,8 +26,12 @@ await _retryService.RetryAsync( var buffer = System.Text.Encoding.UTF8.GetBytes(myContent); var byteContent = new ByteArrayContent(buffer); httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Umb-Webhook-Event", eventName); + foreach (KeyValuePair header in webhook.Headers) + { + httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); + } - return await httpClient.PostAsync(url, byteContent); + return await httpClient.PostAsync(webhook.Url, byteContent); }, _maxRetries); } diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs index d0f8f12207c2..52622a066935 100644 --- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -22,10 +22,5 @@ public class WebhookViewModel public bool Enabled { get; set; } [DataMember(Name = "headers")] - public Dictionary Headers { get; set; } = new() - { - {"HeaderOne", "HeaderOneValue"}, - {"HeaderTwo", "HeaderTwoValue"}, - {"HeaderThree", "HeaderThreeValue"}, - }; + public Dictionary Headers { get; set; } = new(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js index 09efa64ea614..2ce2649e0668 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js @@ -43,6 +43,9 @@ size: 'small', position: 'right', submit(model) { + if(!$scope.model.webhook.headers){ + $scope.model.webhook.headers = {}; + } $scope.model.webhook.headers[model.key] = model.value; editorService.close(); }, From 50765f326c5a957518bce5f6079a794b0f5e9932 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 18 Oct 2023 10:46:56 +0200 Subject: [PATCH 072/102] Refactor retry service to also retry on non success status codes. --- .../DependencyInjection/UmbracoBuilder.cs | 1 + .../Models/WebhookResponseModel.cs | 2 +- .../Services/IWebhookLogFactory.cs | 9 ++++++ .../Services/WebhookLogFactory.cs | 30 +++++++++++++++++++ .../Webhooks/ContentDeleteWebhookEvent.cs | 4 +-- .../Webhooks/ContentPublishWebhookEvent.cs | 4 +-- .../Webhooks/ContentUnpublishWebhookEvent.cs | 4 +-- .../Webhooks/MediaDeleteWebhookEvent.cs | 4 +-- .../Webhooks/MediaSaveWebhookEvent.cs | 4 +-- src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 17 +++-------- src/Umbraco.Core/Webhooks/WebhookLog.cs | 2 +- .../Factories/WebhookLogFactory.cs | 2 +- .../Services/Implement/RetryService.cs | 21 +++++++++---- .../Mapping/WebhookMapDefinition.cs | 2 +- 14 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 src/Umbraco.Core/Services/IWebhookLogFactory.cs create mode 100644 src/Umbraco.Core/Services/WebhookLogFactory.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index abc89ee15b52..b47a69912d28 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -329,6 +329,7 @@ private void AddCoreServices() Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddNotificationAsyncHandler(); Services.AddNotificationAsyncHandler(); Services.AddNotificationAsyncHandler(); diff --git a/src/Umbraco.Core/Models/WebhookResponseModel.cs b/src/Umbraco.Core/Models/WebhookResponseModel.cs index 321ebdaa9288..1f4044380695 100644 --- a/src/Umbraco.Core/Models/WebhookResponseModel.cs +++ b/src/Umbraco.Core/Models/WebhookResponseModel.cs @@ -2,7 +2,7 @@ public class WebhookResponseModel { - public HttpResponseMessage HttpResponseMessage { get; set; } = null!; + public HttpResponseMessage? HttpResponseMessage { get; set; } public int RetryCount { get; set; } } diff --git a/src/Umbraco.Core/Services/IWebhookLogFactory.cs b/src/Umbraco.Core/Services/IWebhookLogFactory.cs new file mode 100644 index 000000000000..fa600dda82d6 --- /dev/null +++ b/src/Umbraco.Core/Services/IWebhookLogFactory.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebhookLogFactory +{ + Task CreateAsync(string eventName, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken); +} diff --git a/src/Umbraco.Core/Services/WebhookLogFactory.cs b/src/Umbraco.Core/Services/WebhookLogFactory.cs new file mode 100644 index 000000000000..62a900953ca0 --- /dev/null +++ b/src/Umbraco.Core/Services/WebhookLogFactory.cs @@ -0,0 +1,30 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Services; + +public class WebhookLogFactory : IWebhookLogFactory +{ + public async Task CreateAsync(string eventName, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken) + { + var log = new WebhookLog + { + Date = DateTime.UtcNow, + EventName = eventName, + Key = Guid.NewGuid(), + Url = webhook.Url, + }; + + if (responseModel.HttpResponseMessage is not null) + { + log.RequestBody = await responseModel.HttpResponseMessage!.RequestMessage!.Content!.ReadAsStringAsync(cancellationToken); + log.ResponseBody = await responseModel.HttpResponseMessage.Content.ReadAsStringAsync(cancellationToken); + log.StatusCode = responseModel.HttpResponseMessage.StatusCode.ToString(); + log.RetryCount = responseModel.RetryCount; + log.ResponseHeaders = responseModel.HttpResponseMessage.Headers.ToString(); + log.RequestHeaders = responseModel.HttpResponseMessage.RequestMessage.Headers.ToString(); + } + + return log; + } +} diff --git a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs index be12f30d3910..306c4800cb63 100644 --- a/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentDeleteWebhookEvent.cs @@ -8,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentDeleteWebhookEvent : WebhookEventBase { - public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) - : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.ContentDelete) + public ContentDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings, IWebhookLogFactory webhookLogFactory) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, webhookLogFactory, Constants.WebhookEvents.ContentDelete) { } diff --git a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs index 737d2397cf99..f191a35b3df7 100644 --- a/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentPublishWebhookEvent.cs @@ -8,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentPublishWebhookEvent : WebhookEventBase { - public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) - : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.ContentPublish) + public ContentPublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings, IWebhookLogFactory webhookLogFactory) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, webhookLogFactory, Constants.WebhookEvents.ContentPublish) { } diff --git a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs index 4987c897d6b9..2c9dfbd78933 100644 --- a/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/ContentUnpublishWebhookEvent.cs @@ -8,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class ContentUnpublishWebhookEvent : WebhookEventBase { - public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) - : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.ContentUnpublish) + public ContentUnpublishWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings, IWebhookLogFactory webhookLogFactory) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, webhookLogFactory, Constants.WebhookEvents.ContentUnpublish) { } diff --git a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs index 71203091a0d4..69a05e7d863c 100644 --- a/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaDeleteWebhookEvent.cs @@ -8,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class MediaDeleteWebhookEvent : WebhookEventBase { - public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) - : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.MediaDelete) + public MediaDeleteWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings, IWebhookLogFactory webhookLogFactory) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, webhookLogFactory, Constants.WebhookEvents.MediaDelete) { } diff --git a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs index 20f14f4c475b..467ad0688954 100644 --- a/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/MediaSaveWebhookEvent.cs @@ -8,8 +8,8 @@ namespace Umbraco.Cms.Core.Webhooks; public class MediaSaveWebhookEvent : WebhookEventBase { - public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings) - : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, Constants.WebhookEvents.MediaSave) + public MediaSaveWebhookEvent(IWebhookFiringService webhookFiringService, IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings, IWebhookLogFactory webhookLogFactory) + : base(webhookFiringService, webHookService, webhookLogService, webhookSettings, webhookLogFactory, Constants.WebhookEvents.MediaSave) { } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs index 5c6f1ed35e66..54099251ecfa 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -15,6 +15,7 @@ public abstract class WebhookEventBase : IWebhookEvent, private readonly IWebhookFiringService _webhookFiringService; private readonly IWebHookService _webHookService; private readonly IWebhookLogService _webhookLogService; + private readonly IWebhookLogFactory _webhookLogFactory; private WebhookSettings _webhookSettings; protected WebhookEventBase( @@ -22,11 +23,13 @@ protected WebhookEventBase( IWebHookService webHookService, IWebhookLogService webhookLogService, IOptionsMonitor webhookSettings, + IWebhookLogFactory webhookLogFactory, string eventName) { _webhookFiringService = webhookFiringService; _webHookService = webHookService; _webhookLogService = webhookLogService; + _webhookLogFactory = webhookLogFactory; EventName = eventName; _webhookSettings = webhookSettings.CurrentValue; webhookSettings.OnChange(x => _webhookSettings = x); @@ -59,19 +62,7 @@ public async Task HandleAsync(TNotification notification, CancellationToken canc WebhookResponseModel response = await _webhookFiringService.Fire(webhook, EventName, entity); - var log = new WebhookLog - { - Date = DateTime.UtcNow, - EventName = EventName, - RequestBody = await response.HttpResponseMessage.RequestMessage!.Content!.ReadAsStringAsync(cancellationToken), - ResponseBody = await response.HttpResponseMessage.Content.ReadAsStringAsync(cancellationToken), - StatusCode = response.HttpResponseMessage.StatusCode.ToString(), - RetryCount = response.RetryCount, - Key = Guid.NewGuid(), - Url = webhook.Url, - ResponseHeaders = response.HttpResponseMessage.Headers.ToString(), - RequestHeaders = response.HttpResponseMessage.RequestMessage.Headers.ToString(), - }; + WebhookLog log = await _webhookLogFactory.CreateAsync(EventName, response, webhook, cancellationToken); await _webhookLogService.CreateAsync(log); } } diff --git a/src/Umbraco.Core/Webhooks/WebhookLog.cs b/src/Umbraco.Core/Webhooks/WebhookLog.cs index 04b25e3873ab..14fd6dea0cae 100644 --- a/src/Umbraco.Core/Webhooks/WebhookLog.cs +++ b/src/Umbraco.Core/Webhooks/WebhookLog.cs @@ -18,7 +18,7 @@ public class WebhookLog public string RequestHeaders { get; set; } = string.Empty; - public string RequestBody { get; set; } = string.Empty; + public string? RequestBody { get; set; } = string.Empty; public string ResponseHeaders { get; set; } = string.Empty; diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs index 687b08c61424..1cd78a865dca 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs @@ -10,7 +10,7 @@ public static WebhookLogDto CreateDto(WebhookLog log) => { Date = log.Date, EventName = log.EventName, - RequestBody = log.RequestBody, + RequestBody = log.RequestBody ?? string.Empty, ResponseBody = log.ResponseBody, RetryCount = log.RetryCount, StatusCode = log.StatusCode, diff --git a/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs b/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs index 9bffa2eaf6fe..e2a760768124 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/RetryService.cs @@ -12,11 +12,21 @@ public async Task RetryAsync(Func RetryAsync(Func Date: Thu, 26 Oct 2023 12:12:30 +0200 Subject: [PATCH 073/102] Refactor registration of Webhooks, to also register as NotificationHandler --- .../UmbracoBuilder.Collections.cs | 7 +- .../DependencyInjection/UmbracoBuilder.cs | 5 -- .../Webhooks/WebhookEventCollectionBuilder.cs | 72 ++++++++++++++++++- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index abcaa16fee40..e6b413b07fc8 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -129,12 +129,7 @@ internal static void AddAllCoreCollectionBuilders(this IUmbracoBuilder builder) builder.FilterHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.SortHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.ContentIndexHandlers().Add(() => builder.TypeLoader.GetTypes()); - builder.WebhookEvents() - .Append() - .Append() - .Append() - .Append() - .Append(); + builder.WebhookEvents().AddCoreWebhooks(); } /// diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index b47a69912d28..dd3344c3898a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -330,11 +330,6 @@ private void AddCoreServices() Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); - Services.AddNotificationAsyncHandler(); - Services.AddNotificationAsyncHandler(); - Services.AddNotificationAsyncHandler(); - Services.AddNotificationAsyncHandler(); - Services.AddNotificationAsyncHandler(); } } } diff --git a/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs b/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs index 87b9da8b45bf..5c0577aeff31 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs @@ -1,8 +1,78 @@ -using Umbraco.Cms.Core.Composing; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Core.Webhooks; public class WebhookEventCollectionBuilder : OrderedCollectionBuilderBase { protected override WebhookEventCollectionBuilder This => this; + + public override void RegisterWith(IServiceCollection services) + { + // register the collection + services.Add(new ServiceDescriptor(typeof(WebhookEventCollection), CreateCollection, ServiceLifetime.Singleton)); + + // register the types + RegisterTypes(services); + } + + public WebhookEventCollectionBuilder AddCoreWebhooks() + { + Append(); + Append(); + Append(); + Append(); + Append(); + return this; + } + + private void RegisterTypes(IServiceCollection services) + { + Type[] types = GetRegisteringTypes(GetTypes()).ToArray(); + + // ensure they are safe + foreach (Type type in types) + { + EnsureType(type, "register"); + } + + // register them - ensuring that each item is registered with the same lifetime as the collection. + // NOTE: Previously each one was not registered with the same lifetime which would mean that if there + // was a dependency on an individual item, it would resolve a brand new transient instance which isn't what + // we would expect to happen. The same item should be resolved from the container as the collection. + foreach (Type type in types) + { + Type notificationType = GetNotificationType(type); // Implement a method to extract the TNotification type from the INotificationHandler + + var descriptor = new ServiceDescriptor( + typeof(INotificationAsyncHandler<>).MakeGenericType(notificationType), + type, + ServiceLifetime.Transient); + + if (!services.Contains(descriptor)) + { + services.Add(descriptor); + } + } + } + + private Type GetNotificationType(Type handlerType) + { + if (handlerType.BaseType != null && handlerType.BaseType.IsGenericType && handlerType.BaseType.GetGenericTypeDefinition() == typeof(WebhookEventBase<,>)) + { + Type[] genericArguments = handlerType.BaseType.GetGenericArguments(); + + Type? notificationType = genericArguments.FirstOrDefault(arg => typeof(INotification).IsAssignableFrom(arg)); + + if (notificationType is not null) + { + return notificationType; + } + } + + throw new InvalidOperationException($"Invalid handlerType: {handlerType}"); + } } From 6078d661a425153a6e77b24347c1844b8b7c1d2a Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 26 Oct 2023 13:26:15 +0200 Subject: [PATCH 074/102] Add webhooks migration --- .../Webhooks/WebhookEventCollectionBuilder.cs | 1 + .../Migrations/Upgrade/UmbracoPlan.cs | 3 ++ .../Upgrade/V_13_0_0/AddWebhooks.cs | 51 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhooks.cs diff --git a/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs b/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs index 5c0577aeff31..98199cf6b789 100644 --- a/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs +++ b/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs @@ -17,6 +17,7 @@ public override void RegisterWith(IServiceCollection services) // register the types RegisterTypes(services); + base.RegisterWith(services); } public WebhookEventCollectionBuilder AddCoreWebhooks() diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 2844d53d80ea..306f7869f726 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -94,5 +94,8 @@ protected virtual void DefinePlan() // And once more for 12 To("{2D4C9FBD-08B3-472D-A76C-6ED467A0CD20}"); + + // To 13.0.0 + To("{C76D9C9A-635B-4D2C-A301-05642A523E9D}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhooks.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhooks.cs new file mode 100644 index 000000000000..3a4441ecdea1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhooks.cs @@ -0,0 +1,51 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; + +public class AddWebhooks : MigrationBase +{ + public AddWebhooks(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Webhook)) + { + return; + } + + Create.Table().Do(); + + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Event2Webhook)) + { + return; + } + + Create.Table().Do(); + + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.EntityKey2Webhook)) + { + return; + } + + Create.Table().Do(); + + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Headers2Webhook)) + { + return; + } + + Create.Table().Do(); + + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.WebhookLog)) + { + return; + } + + Create.Table().Do(); + } +} From 9339f49787f0f101d5be75fc84cb149706df96bf Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 26 Oct 2023 13:55:45 +0200 Subject: [PATCH 075/102] Add key for adding webhook headers --- src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml | 1 + src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index af415632928a..da6bb95dbfa4 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1977,6 +1977,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Create webhook + Add webhook header Logs Add Document Type Add Media Type diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html index e9287792e84b..940f5e6ef6e0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html @@ -110,7 +110,7 @@ + label-key="webhooks_addWebhookHeader">
From cd751a69655080af86643a6fd661b441b6c67a5f Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 26 Oct 2023 15:39:26 +0200 Subject: [PATCH 076/102] Fix up test --- .../Umbraco.Core/Composing/TypeFinderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeFinderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeFinderTests.cs index 3089d898938b..047e28dda5e8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeFinderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeFinderTests.cs @@ -55,10 +55,10 @@ public void Find_Classes_With_Attribute() Assert.AreEqual(0, typesFound.Count()); // 0 classes in _assemblies are marked with [Tree] typesFound = typeFinder.FindClassesWithAttribute(new[] { typeof(TreeAttribute).Assembly }); - Assert.AreEqual(23, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] + Assert.AreEqual(24, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] typesFound = typeFinder.FindClassesWithAttribute(); - Assert.AreEqual(23, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] + Assert.AreEqual(24, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] } [AttributeUsage(AttributeTargets.Class)] From 28b1a8a32a75c8687e1aff161eff006dd579ea9c Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:51:33 +0200 Subject: [PATCH 077/102] Change event icon to flag --- .../views/common/infiniteeditors/eventpicker/eventpicker.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.html index de497e7ffe83..d4033784ed83 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.html @@ -17,7 +17,7 @@