diff --git a/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs b/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs
index b772a103ba..2bfb5a2375 100644
--- a/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs
@@ -7,6 +7,7 @@ public class WebhookSettings
{
private const bool StaticEnabled = true;
private const int StaticMaximumRetries = 5;
+ internal const string StaticPeriod = "00:00:10";
///
/// Gets or sets a value indicating whether webhooks are enabled.
@@ -31,4 +32,10 @@ public class WebhookSettings
///
[DefaultValue(StaticMaximumRetries)]
public int MaximumRetries { get; set; } = StaticMaximumRetries;
+
+ ///
+ /// Gets or sets a value for the period of the webhook firing.
+ ///
+ [DefaultValue(StaticPeriod)]
+ public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod);
}
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
index 354ef7a40c..c8ef1fbff7 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
@@ -35,13 +35,11 @@ using Umbraco.Cms.Core.Runtime;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
-using Umbraco.Cms.Core.Snippets;
using Umbraco.Cms.Core.DynamicRoot;
using Umbraco.Cms.Core.Sync;
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
@@ -332,9 +330,12 @@ namespace Umbraco.Cms.Core.DependencyInjection
// Register filestream security analyzers
Services.AddUnique();
Services.AddUnique();
+
+ // Register Webhook services
Services.AddUnique();
Services.AddUnique();
Services.AddUnique();
+ Services.AddUnique();
}
}
}
diff --git a/src/Umbraco.Core/Models/WebhookRequest.cs b/src/Umbraco.Core/Models/WebhookRequest.cs
new file mode 100644
index 0000000000..b7f2d58284
--- /dev/null
+++ b/src/Umbraco.Core/Models/WebhookRequest.cs
@@ -0,0 +1,14 @@
+namespace Umbraco.Cms.Core.Models;
+
+public class WebhookRequest
+{
+ public int Id { get; set; }
+
+ public Guid WebhookKey { get; set; }
+
+ public string EventAlias { get; set; } = string.Empty;
+
+ public string? RequestObject { get; set; }
+
+ public int RetryCount { get; set; }
+}
diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
index cdc5af450a..631e99df6e 100644
--- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
+++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
@@ -91,6 +91,7 @@ public static partial class Constants
public const string Webhook2Events = Webhook + "2Events";
public const string Webhook2Headers = Webhook + "2Headers";
public const string WebhookLog = Webhook + "Log";
+ public const string WebhookRequest = Webhook + "Request";
}
}
}
diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs
index e97f16a663..7672d73ec7 100644
--- a/src/Umbraco.Core/Persistence/Constants-Locks.cs
+++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs
@@ -70,5 +70,10 @@ public static partial class Constants
/// ScheduledPublishing job.
///
public const int ScheduledPublishing = -341;
+
+ ///
+ /// ScheduledPublishing job.
+ ///
+ public const int WebhookRequest = -342;
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRequestRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRequestRepository.cs
new file mode 100644
index 0000000000..1a2b7b158d
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRequestRepository.cs
@@ -0,0 +1,33 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Persistence.Repositories;
+
+public interface IWebhookRequestRepository
+{
+ ///
+ /// Creates a webhook request in the current repository.
+ ///
+ /// The webhook you want to create.
+ /// The created webhook
+ Task CreateAsync(WebhookRequest webhookRequest);
+
+ ///
+ /// Deletes a webhook request in the current repository
+ ///
+ /// The webhook request to be deleted.
+ /// A representing the asynchronous operation.
+ Task DeleteAsync(WebhookRequest webhookRequest);
+
+ ///
+ /// Gets all of the webhook requests in the current repository.
+ ///
+ /// A paged model of objects.
+ Task> GetAllAsync();
+
+ ///
+ /// Update a webhook request in the current repository.
+ ///
+ /// The webhook request you want to update.
+ /// The updated webhook
+ Task UpdateAsync(WebhookRequest webhookRequest);
+}
diff --git a/src/Umbraco.Core/Services/IWebhookRequestService.cs b/src/Umbraco.Core/Services/IWebhookRequestService.cs
new file mode 100644
index 0000000000..b3b6396b01
--- /dev/null
+++ b/src/Umbraco.Core/Services/IWebhookRequestService.cs
@@ -0,0 +1,35 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services;
+
+public interface IWebhookRequestService
+{
+ ///
+ /// Creates a webhook request.
+ ///
+ /// The key of the webhook.
+ /// The alias of the event that is creating the request.
+ /// The payload you want to send with your request.
+ /// The created webhook
+ Task CreateAsync(Guid webhookKey, string eventAlias, object? payload);
+
+ ///
+ /// Gets all of the webhook requests in the current database.
+ ///
+ /// An enumerable of objects.
+ Task> GetAllAsync();
+
+ ///
+ /// Deletes a webhook request
+ ///
+ /// The webhook request to be deleted.
+ /// A representing the asynchronous operation.
+ Task DeleteAsync(WebhookRequest webhookRequest);
+
+ ///
+ /// Update a webhook request.
+ ///
+ /// The webhook request you want to update.
+ /// The updated webhook
+ Task UpdateAsync(WebhookRequest webhookRequest);
+}
diff --git a/src/Umbraco.Core/Services/WebhookRequestService.cs b/src/Umbraco.Core/Services/WebhookRequestService.cs
new file mode 100644
index 0000000000..249ed21421
--- /dev/null
+++ b/src/Umbraco.Core/Services/WebhookRequestService.cs
@@ -0,0 +1,63 @@
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Serialization;
+
+namespace Umbraco.Cms.Core.Services;
+
+public class WebhookRequestService : IWebhookRequestService
+{
+ private readonly ICoreScopeProvider _coreScopeProvider;
+ private readonly IWebhookRequestRepository _webhookRequestRepository;
+ private readonly IJsonSerializer _jsonSerializer;
+
+ public WebhookRequestService(ICoreScopeProvider coreScopeProvider, IWebhookRequestRepository webhookRequestRepository, IJsonSerializer jsonSerializer)
+ {
+ _coreScopeProvider = coreScopeProvider;
+ _webhookRequestRepository = webhookRequestRepository;
+ _jsonSerializer = jsonSerializer;
+ }
+
+ public async Task CreateAsync(Guid webhookKey, string eventAlias, object? payload)
+ {
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ scope.WriteLock(Constants.Locks.WebhookRequest);
+ var webhookRequest = new WebhookRequest
+ {
+ WebhookKey = webhookKey,
+ EventAlias = eventAlias,
+ RequestObject = _jsonSerializer.Serialize(payload),
+ RetryCount = 0,
+ };
+
+ webhookRequest = await _webhookRequestRepository.CreateAsync(webhookRequest);
+ scope.Complete();
+
+ return webhookRequest;
+ }
+
+ public Task> GetAllAsync()
+ {
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ Task> webhookRequests = _webhookRequestRepository.GetAllAsync();
+ scope.Complete();
+ return webhookRequests;
+ }
+
+ public async Task DeleteAsync(WebhookRequest webhookRequest)
+ {
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ scope.WriteLock(Constants.Locks.WebhookRequest);
+ await _webhookRequestRepository.DeleteAsync(webhookRequest);
+ scope.Complete();
+ }
+
+ public async Task UpdateAsync(WebhookRequest webhookRequest)
+ {
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ scope.WriteLock(Constants.Locks.WebhookRequest);
+ WebhookRequest updated = await _webhookRequestRepository.UpdateAsync(webhookRequest);
+ scope.Complete();
+ return updated;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs
new file mode 100644
index 0000000000..fe8cf1e204
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs
@@ -0,0 +1,124 @@
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+
+public class WebhookFiring : IRecurringBackgroundJob
+{
+ private readonly ILogger _logger;
+ private readonly IWebhookRequestService _webhookRequestService;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IWebhookLogFactory _webhookLogFactory;
+ private readonly IWebhookLogService _webhookLogService;
+ private readonly IWebhookService _webHookService;
+ private readonly ICoreScopeProvider _coreScopeProvider;
+ private WebhookSettings _webhookSettings;
+
+ public TimeSpan Period => _webhookSettings.Period;
+
+ public TimeSpan Delay { get; } = TimeSpan.FromSeconds(20);
+
+ // No-op event as the period never changes on this job
+ public event EventHandler PeriodChanged { add { } remove { } }
+
+ public WebhookFiring(
+ ILogger logger,
+ IWebhookRequestService webhookRequestService,
+ IJsonSerializer jsonSerializer,
+ IWebhookLogFactory webhookLogFactory,
+ IWebhookLogService webhookLogService,
+ IWebhookService webHookService,
+ IOptionsMonitor webhookSettings,
+ ICoreScopeProvider coreScopeProvider)
+ {
+ _logger = logger;
+ _webhookRequestService = webhookRequestService;
+ _jsonSerializer = jsonSerializer;
+ _webhookLogFactory = webhookLogFactory;
+ _webhookLogService = webhookLogService;
+ _webHookService = webHookService;
+ _coreScopeProvider = coreScopeProvider;
+ _webhookSettings = webhookSettings.CurrentValue;
+ webhookSettings.OnChange(x => _webhookSettings = x);
+ }
+
+ public async Task RunJobAsync()
+ {
+ IEnumerable requests;
+ using (ICoreScope scope = _coreScopeProvider.CreateCoreScope())
+ {
+ scope.ReadLock(Constants.Locks.WebhookRequest);
+ requests = await _webhookRequestService.GetAllAsync();
+ scope.Complete();
+ }
+
+ await Task.WhenAll(requests.Select(request =>
+ {
+ using (ExecutionContext.SuppressFlow())
+ {
+ return Task.Run(async () =>
+ {
+ Webhook? webhook = await _webHookService.GetAsync(request.WebhookKey);
+ if (webhook is null)
+ {
+ return;
+ }
+
+ HttpResponseMessage? response = await SendRequestAsync(webhook, request.EventAlias, request.RequestObject, request.RetryCount, CancellationToken.None);
+
+ if ((response?.IsSuccessStatusCode ?? false) || request.RetryCount >= _webhookSettings.MaximumRetries)
+ {
+ await _webhookRequestService.DeleteAsync(request);
+ }
+ else
+ {
+ request.RetryCount++;
+ await _webhookRequestService.UpdateAsync(request);
+ }
+ });
+ }
+ }));
+ }
+
+ private async Task SendRequestAsync(Webhook webhook, string eventName, object? payload, int retryCount, CancellationToken cancellationToken)
+ {
+ using var httpClient = new HttpClient();
+
+ var serializedObject = _jsonSerializer.Serialize(payload);
+ var stringContent = new StringContent(serializedObject, Encoding.UTF8, "application/json");
+ stringContent.Headers.TryAddWithoutValidation("Umb-Webhook-Event", eventName);
+
+ foreach (KeyValuePair header in webhook.Headers)
+ {
+ stringContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+
+ HttpResponseMessage? response = null;
+ try
+ {
+ response = await httpClient.PostAsync(webhook.Url, stringContent, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while sending webhook request for webhook {WebhookKey}.", webhook);
+ }
+
+ var webhookResponseModel = new WebhookResponseModel
+ {
+ HttpResponseMessage = response,
+ RetryCount = retryCount,
+ };
+
+ WebhookLog log = await _webhookLogFactory.CreateAsync(eventName, webhookResponseModel, webhook, cancellationToken);
+ await _webhookLogService.CreateAsync(log);
+
+ return response;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
index 2e4832cff0..8c9f4b1193 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
@@ -73,6 +73,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
return builder;
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index a3f685f0ae..2d17d42f83 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -993,31 +993,19 @@ internal class DatabaseDataCreator
private void CreateLockData()
{
// all lock objects
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.Servers, Name = "Servers" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.ContentTypes, Name = "ContentTypes" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.ContentTree, Name = "ContentTree" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.MediaTypes, Name = "MediaTypes" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.MediaTree, Name = "MediaTree" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.MemberTypes, Name = "MemberTypes" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.MemberTree, Name = "MemberTree" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.Domains, Name = "Domains" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.Languages, Name = "Languages" });
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
-
- _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false,
- new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Servers, Name = "Servers" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.ContentTypes, Name = "ContentTypes" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.ContentTree, Name = "ContentTree" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MediaTypes, Name = "MediaTypes" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MediaTree, Name = "MediaTree" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MemberTypes, Name = "MemberTypes" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MemberTree, Name = "MemberTree" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Languages, Name = "Languages" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.WebhookRequest, Name = "WebhookRequest" });
}
private void CreateContentTypeData()
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
index 9a944e4a90..fdd8a07256 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
@@ -88,6 +88,7 @@ public class DatabaseSchemaCreator
typeof(Webhook2EventsDto),
typeof(Webhook2HeadersDto),
typeof(WebhookLogDto),
+ typeof(WebhookRequestDto),
};
private readonly IUmbracoDatabase _database;
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index 3a4e715228..e543c61279 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -98,5 +98,7 @@ public class UmbracoPlan : MigrationPlan
// To 13.0.0
To("{C76D9C9A-635B-4D2C-A301-05642A523E9D}");
To("{D5139400-E507-4259-A542-C67358F7E329}");
+ To("{4E652F18-9A29-4656-A899-E3F39069C47E}");
+ To("{148714C8-FE0D-4553-B034-439D91468761}");
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhookRequest.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhookRequest.cs
new file mode 100644
index 0000000000..458cd26b31
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhookRequest.cs
@@ -0,0 +1,35 @@
+using NPoco;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Infrastructure.Persistence;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0;
+
+public class AddWebhookRequest : MigrationBase
+{
+ public AddWebhookRequest(IMigrationContext context) : base(context)
+ {
+ }
+
+ protected override void Migrate()
+ {
+ IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray();
+ if (tables.InvariantContains(Constants.DatabaseSchema.Tables.WebhookRequest) is false)
+ {
+ Create.Table().Do();
+ }
+
+ Sql sql = Database.SqlContext.Sql()
+ .Select()
+ .From()
+ .Where(x => x.Id == Constants.Locks.WebhookRequest);
+
+ LockDto? webhookRequestLock = Database.FirstOrDefault(sql);
+
+ if (webhookRequestLock is null)
+ {
+ Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.WebhookRequest, Name = "WebhookRequest" });
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/RenameWebhookIdToKey.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/RenameWebhookIdToKey.cs
new file mode 100644
index 0000000000..e68aa258a9
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/RenameWebhookIdToKey.cs
@@ -0,0 +1,27 @@
+using Umbraco.Cms.Core;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0;
+
+public class RenameWebhookIdToKey : MigrationBase
+{
+ public RenameWebhookIdToKey(IMigrationContext context) : base(context)
+ {
+ }
+
+ protected override void Migrate()
+ {
+ // This check is here because we renamed a column from 13-rc1 to 13-rc2, the previous migration adds the table
+ // so if you are upgrading from 13-rc1 to 13-rc2 then this column will not exist.
+ // If you are however upgrading from 12, then this column will exist, and thus there is no need to rename it.
+ if (ColumnExists(Constants.DatabaseSchema.Tables.WebhookLog, "webhookId") is false)
+ {
+ return;
+ }
+
+ Rename
+ .Column("webhookId")
+ .OnTable(Constants.DatabaseSchema.Tables.WebhookLog)
+ .To("webhookKey")
+ .Do();
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
index a9588ecb7d..3f3e5623b7 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
@@ -13,7 +13,7 @@ internal class WebhookLogDto
[PrimaryKeyColumn(AutoIncrement = true)]
public int Id { get; set; }
- [Column("webhookId")]
+ [Column("webhookKey")]
public Guid WebhookKey { get; set; }
[Column(Name = "key")]
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookRequestDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookRequestDto.cs
new file mode 100644
index 0000000000..e1f55ad042
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookRequestDto.cs
@@ -0,0 +1,28 @@
+using NPoco;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
+[TableName(Constants.DatabaseSchema.Tables.WebhookRequest)]
+[PrimaryKey("id")]
+[ExplicitColumns]
+public class WebhookRequestDto
+{
+ [Column("id")]
+ [PrimaryKeyColumn(AutoIncrement = true)]
+ public int Id { get; set; }
+
+ [Column("webhookKey")]
+ public Guid WebhookKey { get; set; }
+
+ [Column("eventName")]
+ public string Alias { get; set; } = string.Empty;
+
+ [Column(Name = "requestObject")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public string? RequestObject { get; set; }
+
+ [Column("retryCount")]
+ public int RetryCount { get; set; }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookRequestFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookRequestFactory.cs
new file mode 100644
index 0000000000..dd0648588b
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookRequestFactory.cs
@@ -0,0 +1,27 @@
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Factories;
+
+internal static class WebhookRequestFactory
+{
+ public static WebhookRequestDto CreateDto(WebhookRequest webhookRequest) =>
+ new()
+ {
+ Alias = webhookRequest.EventAlias,
+ Id = webhookRequest.Id,
+ WebhookKey = webhookRequest.WebhookKey,
+ RequestObject = webhookRequest.RequestObject,
+ RetryCount = webhookRequest.RetryCount,
+ };
+
+ public static WebhookRequest CreateModel(WebhookRequestDto webhookRequestDto) =>
+ new()
+ {
+ EventAlias = webhookRequestDto.Alias,
+ Id = webhookRequestDto.Id,
+ WebhookKey = webhookRequestDto.WebhookKey,
+ RequestObject = webhookRequestDto.RequestObject,
+ RetryCount = webhookRequestDto.RetryCount,
+ };
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRequestRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRequestRepository.cs
new file mode 100644
index 0000000000..21b25c77a7
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRequestRepository.cs
@@ -0,0 +1,65 @@
+using NPoco;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Repositories;
+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;
+
+public class WebhookRequestRepository : IWebhookRequestRepository
+{
+ private readonly IScopeAccessor _scopeAccessor;
+
+ public WebhookRequestRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor;
+
+ private IUmbracoDatabase Database
+ {
+ get
+ {
+ if (_scopeAccessor.AmbientScope is null)
+ {
+ throw new NotSupportedException("Need to be executed in a scope");
+ }
+
+ return _scopeAccessor.AmbientScope.Database;
+ }
+ }
+
+ public async Task CreateAsync(WebhookRequest webhookRequest)
+ {
+ WebhookRequestDto dto = WebhookRequestFactory.CreateDto(webhookRequest);
+ var result = await Database.InsertAsync(dto);
+ var id = Convert.ToInt32(result);
+ webhookRequest.Id = id;
+ return webhookRequest;
+ }
+
+ public async Task DeleteAsync(WebhookRequest webhookRequest)
+ {
+ Sql sql = Database.SqlContext.Sql()
+ .Delete()
+ .Where(x => x.Id == webhookRequest.Id);
+
+ await Database.ExecuteAsync(sql);
+ }
+
+ public async Task> GetAllAsync()
+ {
+ Sql? sql = Database.SqlContext.Sql()
+ .Select()
+ .From();
+
+ List webhookDtos = await Database.FetchAsync(sql);
+
+ return webhookDtos.Select(WebhookRequestFactory.CreateModel);
+ }
+
+ public async Task UpdateAsync(WebhookRequest webhookRequest)
+ {
+ WebhookRequestDto dto = WebhookRequestFactory.CreateDto(webhookRequest);
+ await Database.UpdateAsync(dto);
+ return webhookRequest;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs
index 67b55c913e..b107d82999 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs
@@ -1,74 +1,16 @@
-using System.Net.Http.Headers;
-using System.Text;
-using Microsoft.Extensions.Options;
-using Umbraco.Cms.Core.Configuration.Models;
-using Umbraco.Cms.Core.Models;
-using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Infrastructure.Services.Implement;
public class WebhookFiringService : IWebhookFiringService
{
- private readonly IJsonSerializer _jsonSerializer;
- private readonly WebhookSettings _webhookSettings;
- private readonly IWebhookLogService _webhookLogService;
- private readonly IWebhookLogFactory _webhookLogFactory;
+ private readonly IWebhookRequestService _webhookRequestService;
- public WebhookFiringService(
- IJsonSerializer jsonSerializer,
- IOptions webhookSettings,
- IWebhookLogService webhookLogService,
- IWebhookLogFactory webhookLogFactory)
- {
- _jsonSerializer = jsonSerializer;
- _webhookLogService = webhookLogService;
- _webhookLogFactory = webhookLogFactory;
- _webhookSettings = webhookSettings.Value;
- }
+ public WebhookFiringService(IWebhookRequestService webhookRequestService) => _webhookRequestService = webhookRequestService;
- // TODO: Add queing instead of processing directly in thread
- // as this just makes save and publish longer
- public async Task FireAsync(Webhook webhook, string eventAlias, object? payload, CancellationToken cancellationToken)
- {
- for (var retry = 0; retry < _webhookSettings.MaximumRetries; retry++)
- {
- HttpResponseMessage response = await SendRequestAsync(webhook, eventAlias, payload, retry, cancellationToken);
-
- if (response.IsSuccessStatusCode)
- {
- return;
- }
- }
- }
-
- private async Task SendRequestAsync(Webhook webhook, string eventAlias, object? payload, int retryCount, CancellationToken cancellationToken)
- {
- using var httpClient = new HttpClient();
-
- var serializedObject = _jsonSerializer.Serialize(payload);
- var stringContent = new StringContent(serializedObject, Encoding.UTF8, "application/json");
- stringContent.Headers.TryAddWithoutValidation("Umb-Webhook-Event", eventAlias);
-
- foreach (KeyValuePair header in webhook.Headers)
- {
- stringContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
- }
-
- HttpResponseMessage response = await httpClient.PostAsync(webhook.Url, stringContent, cancellationToken);
-
- var webhookResponseModel = new WebhookResponseModel
- {
- HttpResponseMessage = response,
- RetryCount = retryCount,
- };
-
-
- WebhookLog log = await _webhookLogFactory.CreateAsync(eventAlias, webhookResponseModel, webhook, cancellationToken);
- await _webhookLogService.CreateAsync(log);
-
- return response;
- }
+ public async Task FireAsync(Webhook webhook, string eventAlias, object? payload, CancellationToken cancellationToken) =>
+ await _webhookRequestService.CreateAsync(webhook.Key, eventAlias, payload);
}
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs
index 2e48fbf25d..d41ccf684e 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs
@@ -1,6 +1,7 @@
using Examine;
using Examine.Search;
using Lucene.Net.QueryParsers.Classic;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
@@ -8,11 +9,13 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Web.Common.Attributes;
+using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
using SearchResult = Umbraco.Cms.Core.Models.ContentEditing.SearchResult;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
+[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
public class ExamineManagementController : UmbracoAuthorizedJsonController
{
diff --git a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs
index d87398d574..a5e5e3b447 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.Security;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -14,10 +15,12 @@ using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Attributes;
+using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
+[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
public class RedirectUrlManagementController : UmbracoAuthorizedApiController
{
diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
index 71704f4edc..a08f712b37 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -215,6 +215,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddRecurringBackgroundJob();
builder.Services.AddRecurringBackgroundJob();
builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob();
builder.Services.AddRecurringBackgroundJob(provider =>
new ReportSiteJob(
provider.GetRequiredService>(),
@@ -224,7 +225,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddHostedService();
builder.Services.AddSingleton(RecurringBackgroundJobHostedService.CreateHostedServiceFactory);
builder.Services.AddHostedService();
-
+
return builder;
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookRequestServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookRequestServiceTests.cs
new file mode 100644
index 0000000000..3c41aa5ab2
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookRequestServiceTests.cs
@@ -0,0 +1,67 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+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 WebhookRequestServiceTests : UmbracoIntegrationTest
+{
+ private IWebhookRequestService WebhookRequestService => GetRequiredService();
+
+ private IWebhookService WebhookService => GetRequiredService();
+
+ [Test]
+ public async Task Can_Create_And_Get()
+ {
+ var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.Aliases.ContentPublish }));
+ var created = await WebhookRequestService.CreateAsync(createdWebhook.Key, Constants.WebhookEvents.Aliases.ContentPublish, null);
+ var webhooks = await WebhookRequestService.GetAllAsync();
+ var webhook = webhooks.First(x => x.Id == created.Id);
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(created.Id, webhook.Id);
+ Assert.AreEqual(created.EventAlias, webhook.EventAlias);
+ Assert.AreEqual(created.RetryCount, webhook.RetryCount);
+ Assert.AreEqual(created.RequestObject, webhook.RequestObject);
+ Assert.AreEqual(created.WebhookKey, webhook.WebhookKey);
+ });
+ }
+
+ [Test]
+ public async Task Can_Update()
+ {
+ var newRetryCount = 4;
+ var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.Aliases.ContentPublish }));
+ var created = await WebhookRequestService.CreateAsync(createdWebhook.Key, Constants.WebhookEvents.Aliases.ContentPublish, null);
+ created.RetryCount = newRetryCount;
+ await WebhookRequestService.UpdateAsync(created);
+ var webhooks = await WebhookRequestService.GetAllAsync();
+ var webhook = webhooks.First(x => x.Id == created.Id);
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(newRetryCount, webhook.RetryCount);
+ });
+ }
+
+ [Test]
+ public async Task Can_Delete()
+ {
+ var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.Aliases.ContentPublish }));
+ var created = await WebhookRequestService.CreateAsync(createdWebhook.Key, Constants.WebhookEvents.Aliases.ContentPublish, null);
+ await WebhookRequestService.DeleteAsync(created);
+ var webhooks = await WebhookRequestService.GetAllAsync();
+ var webhook = webhooks.FirstOrDefault(x => x.Id == created.Id);
+
+ Assert.Multiple(() =>
+ {
+ Assert.IsNull(webhook);
+ });
+ }
+}